Issues (47)

src/SimpleTask.php (10 issues)

1
<?php
2
3
namespace LeKoala\SimpleJobs;
4
5
use Exception;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\ORM\DataList;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\Security\Member;
10
use SilverStripe\Security\Permission;
11
12
/**
13
 * A simple class to schedule function calls
14
 * They will be picked up by the SimpleJobsController automatically
15
 * and run if the RunDate is below current time
16
 *
17
 * We only run one task each call to avoid excessive usages
18
 * Expect some delays if you have many tasks!
19
 *
20
 * @property ?string $Name
21
 * @property ?string $Task
22
 * @property bool|int $Processed
23
 * @property bool|int $Failed
24
 * @property ?string $ErrorMessage
25
 * @property int $TimeToExecute
26
 * @property int $CallsCount
27
 * @property int $SuccessCalls
28
 * @property int $ErrorCalls
29
 * @property ?string $RunDate
30
 * @property int $OwnerID
31
 * @method \SilverStripe\Security\Member Owner()
32
 * @mixin \SilverStripe\Assets\Shortcodes\FileLinkTracking
33
 * @mixin \SilverStripe\Assets\AssetControlExtension
34
 * @mixin \SilverStripe\CMS\Model\SiteTreeLinkTracking
35
 * @mixin \SilverStripe\Versioned\RecursivePublishable
36
 * @mixin \SilverStripe\Versioned\VersionedStateExtension
37
 */
38
class SimpleTask extends DataObject
39
{
40
41
    /**
42
     * @var string
43
     */
44
    private static $singular_name = 'Scheduled Task';
0 ignored issues
show
The private property $singular_name is not used, and could be removed.
Loading history...
45
46
    /**
47
     * @var string
48
     */
49
    private static $plural_name = 'Scheduled Tasks';
0 ignored issues
show
The private property $plural_name is not used, and could be removed.
Loading history...
50
51
    /**
52
     * @var string
53
     */
54
    private static $table_name = 'SimpleTask'; // When using namespace, specify table name
0 ignored issues
show
The private property $table_name is not used, and could be removed.
Loading history...
55
56
    /**
57
     * @var array<string, string>
58
     */
59
    private static $db = [
0 ignored issues
show
The private property $db is not used, and could be removed.
Loading history...
60
        'Name' => 'Varchar(191)',
61
        'Task' => 'Text',
62
        'Processed' => 'Boolean',
63
        'Failed' => 'Boolean',
64
        'ErrorMessage' => 'Varchar(191)',
65
        'TimeToExecute' => 'Int',
66
        'CallsCount' => 'Int',
67
        'SuccessCalls' => 'Int',
68
        'ErrorCalls' => 'Int',
69
        "RunDate" => "Datetime",
70
    ];
71
72
    /**
73
     * @var array<string, class-string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string>.
Loading history...
74
     */
75
    private static $has_one = [
0 ignored issues
show
The private property $has_one is not used, and could be removed.
Loading history...
76
        'Owner' => Member::class,
77
    ];
78
79
    /**
80
     * @var string
81
     */
82
    private static $default_sort = "RunDate DESC";
0 ignored issues
show
The private property $default_sort is not used, and could be removed.
Loading history...
83
84
    /**
85
     * @var array<string>
86
     */
87
    private static $summary_fields = [
0 ignored issues
show
The private property $summary_fields is not used, and could be removed.
Loading history...
88
        'Created',
89
        'Name',
90
        'Processed',
91
        'Failed',
92
        'TimeToExecute'
93
    ];
94
95
    public function canView($member = null, $context = [])
0 ignored issues
show
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

95
    public function canView($member = null, /** @scrutinizer ignore-unused */ $context = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
96
    {
97
        return Permission::check('CMS_ACCESS_SimpleJobsAdmin', 'any', $member);
98
    }
99
100
    public function canEdit($member = null, $context = [])
0 ignored issues
show
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

100
    public function canEdit($member = null, /** @scrutinizer ignore-unused */ $context = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
101
    {
102
        return Permission::check('CMS_ACCESS_SimpleJobsAdmin', 'any', $member);
103
    }
104
105
    public function canCreate($member = null, $context = [])
106
    {
107
        return Permission::check('CMS_ACCESS_SimpleJobsAdmin', 'any', $member);
108
    }
109
110
    public function canDelete($member = null)
111
    {
112
        return Permission::check('CMS_ACCESS_SimpleJobsAdmin', 'any', $member);
113
    }
114
115
    /**
116
     * @return void
117
     */
118
    protected function onBeforeWrite()
119
    {
120
        parent::onBeforeWrite();
121
122
        // Run asap by default
123
        if (!$this->RunDate) {
124
            $this->RunDate = date('Y-m-d H:i:s');
125
        }
126
127
        $this->CallsCount = count($this->getTaskDetails());
128
    }
129
130
    /**
131
     * An array of entries
132
     * @return array<string, mixed>
133
     */
134
    public function getTaskDetails()
135
    {
136
        if ($this->Task) {
137
            $result = json_decode($this->Task, true);
138
            if ($result) {
139
                return $result;
140
            }
141
        }
142
        return [];
143
    }
144
145
    /**
146
     * @return DataList|SimpleTask[]
147
     */
148
    public static function getTasksThatNeedToRun()
149
    {
150
        $time = date('Y-m-d H:i:s');
151
        return SimpleTask::get()->where("Processed = 0 AND RunDate <= '$time'");
152
    }
153
154
    /**
155
     * @return SimpleTask|null
156
     */
157
    public static function getNextTaskToRun()
158
    {
159
        /** @var SimpleTask|null $task */
160
        $task = self::getTasksThatNeedToRun()->sort('RunDate ASC')->limit(1)->first();
161
        return $task;
162
    }
163
164
    /**
165
     * Append to the list of things to do for this class
166
     *
167
     * @param DataObject $class
168
     * @param string $method
169
     * @param array<mixed> $params
170
     * @return array<mixed>
171
     */
172
    public function addToTask(DataObject $class, $method, $params = [])
173
    {
174
        $details = $this->getTaskDetails();
175
176
        // If no name is set, assume the first method call to be the task
177
        if (!$this->Name) {
178
            $this->Name = $method;
179
        }
180
        // If no owner is set, assume that the member is the owner
181
        if (!$this->OwnerID && $class instanceof Member) {
182
            $this->OwnerID = $class->ID;
183
        }
184
185
        // Task details contain one entry per thing to do in a task
186
        // A task can do multiple calls
187
        $details[] = [
188
            'class' => get_class($class),
189
            'id' => $class->ID,
190
            'function' => $method,
191
            'parameters' => $params
192
        ];
193
194
        $json = json_encode($details);
195
        if ($json) {
196
            $this->Task = $json;
197
        }
198
        return $details;
199
    }
200
201
    /**
202
     * @return bool
203
     */
204
    public function process()
205
    {
206
        if ($this->Processed) {
207
            throw new Exception("Already processed");
208
        }
209
210
        // If there was not enough time, mark the task as processed anyway
211
        // to avoid calling it again next time
212
        $this->Processed = true;
213
        $this->Failed = true;
214
        $this->ErrorMessage = "Task did not complete";
215
        $this->write();
216
217
        $st = time();
218
219
        $conn = DB::get_conn();
220
        $conn->transactionStart();
221
222
        $success = $errors = 0;
223
        try {
224
            $details = $this->getTaskDetails();
225
226
            foreach ($details as $entry) {
227
                /** @var class-string $class */
228
                $class = $entry['class'] ?? '';
229
                /** @var int $id */
230
                $id = $entry['id'];
231
                /** @var string $function */
232
                $function = $entry['function'];
233
                /** @var array<mixed> $parameters */
234
                $parameters = $entry['parameters'];
235
236
                $inst = DataObject::get_by_id($class, $id);
237
238
                $callable = [$inst, $function];
239
                if (!is_callable($callable)) {
240
                    throw new Exception("Not callable $function");
241
                }
242
                if ($inst) {
243
                    $result = call_user_func_array($callable, $parameters);
244
                    if ($result !== false) {
245
                        $success++;
246
                    } else {
247
                        $errors++;
248
                    }
249
                }
250
            }
251
            $this->Failed = false;
252
            $this->ErrorMessage = '';
253
        } catch (Exception $ex) {
254
            $this->Failed = true;
255
            $this->ErrorMessage = $ex->getMessage();
256
        }
257
258
        $et = time();
259
        $tt = $et - $st;
260
261
        $this->SuccessCalls = $success;
262
        $this->ErrorCalls = $errors;
263
        $this->TimeToExecute = $tt;
264
        $this->write();
265
266
        $conn->transactionEnd();
267
268
        return $this->Failed;
269
    }
270
}
271