Completed
Pull Request — master (#109)
by Robbie
01:54
created

QueuedJobService   F

Complexity

Total Complexity 105

Size/Duplication

Total Lines 898
Duplicated Lines 3.23 %

Coupling/Cohesion

Components 1
Dependencies 20

Importance

Changes 0
Metric Value
wmc 105
lcom 1
cbo 20
dl 29
loc 898
rs 1.263
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 17 6
B queueJob() 0 40 5
A startJob() 0 9 3
A copyJobToDescriptor() 0 14 2
B copyDescriptorToJob() 0 28 3
B getNextPendingJob() 0 36 4
B checkJobHealth() 14 46 6
B restartStalledJob() 0 29 2
B initialiseJob() 0 29 3
B grabMutex() 0 22 4
F runJob() 15 227 35
A markStarted() 0 6 2
A hasPassedTimeLimit() 0 15 2
A isMemoryTooHigh() 0 6 2
A getMemoryUsage() 0 7 1
A getMemoryLimit() 0 14 3
A getPHPMemoryLimit() 0 4 1
B parseMemory() 0 15 5
A humanReadable() 0 5 2
A getJobList() 0 7 1
A getJobListFilter() 0 17 3
A runQueue() 0 5 1
C processJobQueue() 0 37 7
A onShutdown() 0 4 1
A getLogger() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like QueuedJobService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueuedJobService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\QueuedJobs\Services;
4
5
use Exception;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Control\Email\Email;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Object;
14
use SilverStripe\Dev\SapphireTest;
15
use SilverStripe\ORM\DataList;
16
use SilverStripe\ORM\DataObject;
17
use SilverStripe\ORM\DB;
18
use SilverStripe\ORM\FieldType\DBDatetime;
19
use SilverStripe\QueuedJobs\DataObjects\QueuedJobDescriptor;
20
use SilverStripe\Security\Member;
21
use SilverStripe\Security\Permission;
22
23
/**
24
 * A service that can be used for starting, stopping and listing queued jobs.
25
 *
26
 * When a job is first added, it is initialised, its job type determined, then persisted to the database
27
 *
28
 * When the queues are scanned, a job is reloaded and processed. Ignoring the persistence and reloading, it looks
29
 * something like
30
 *
31
 * job->getJobType();
32
 * job->getJobData();
33
 * data->write();
34
 * job->setup();
35
 * while !job->isComplete
36
 *  job->process();
37
 *  job->getJobData();
38
 *  data->write();
39
 *
40
 *
41
 * @author Marcus Nyeholt <[email protected]>
42
 * @license BSD http://silverstripe.org/bsd-license/
43
 */
44
class QueuedJobService
45
{
46
    /**
47
     * @var int
48
     */
49
    private static $stall_threshold = 3;
0 ignored issues
show
Unused Code introduced by
The property $stall_threshold is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
50
51
    /**
52
     * How much ram will we allow before pausing and releasing the memory?
53
     *
54
     * For instance, set to 134217728 (128MB) to pause this process if used memory exceeds
55
     * this value. This needs to be set to a value lower than the php_ini max_memory as
56
     * the system will otherwise crash before shutdown can be handled gracefully.
57
     *
58
     * @var int
59
     * @config
60
     */
61
    private static $memory_limit = 134217728;
0 ignored issues
show
Unused Code introduced by
The property $memory_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
62
63
    /**
64
     * Optional time limit (in seconds) to run the service before restarting to release resources.
65
     *
66
     * Defaults to no limit.
67
     *
68
     * @var int
69
     * @config
70
     */
71
    private static $time_limit = 0;
0 ignored issues
show
Unused Code introduced by
The property $time_limit is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
72
73
    /**
74
     * Timestamp (in seconds) when the queue was started
75
     *
76
     * @var int
77
     */
78
    protected $startedAt = 0;
79
80
    /**
81
     * Should "immediate" jobs be managed using the shutdown function?
82
     *
83
     * It is recommended you set up an inotify watch and use that for
84
     * triggering immediate jobs. See the wiki for more information
85
     *
86
     * @var boolean
87
     */
88
    private static $use_shutdown_function = true;
0 ignored issues
show
Unused Code introduced by
The property $use_shutdown_function is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
89
90
    /**
91
     * The location for immediate jobs to be stored in
92
     *
93
     * @var string
94
     */
95
    private static $cache_dir = 'queuedjobs';
0 ignored issues
show
Unused Code introduced by
The property $cache_dir is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
96
97
    /**
98
     * @var DefaultQueueHandler
99
     */
100
    public $queueHandler;
101
102
    /**
103
     *
104
     * @var TaskRunnerEngine
105
     */
106
    public $queueRunner;
107
108
    /**
109
     * Register our shutdown handler
110
     */
111
    public function __construct()
112
    {
113
        // bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
114
        if (Config::inst()->get(__CLASS__, 'use_shutdown_function') && Director::is_cli()) {
115
            if (class_exists('PHPUnit_Framework_TestCase') && SapphireTest::is_running_test()) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
116
                // do NOTHING
117
            } else {
118
                register_shutdown_function(array($this, 'onShutdown'));
119
            }
120
        }
121
        if (Config::inst()->get('SilverStripe\\Control\\Email\\Email', 'queued_job_admin_email') == '') {
122
            Config::inst()->update(
123
                'SilverStripe\\Control\\Email\\Email', 'queued_job_admin_email',
124
                Config::inst()->get('SilverStripe\\Control\\Email\\Email', 'admin_email')
125
            );
126
        }
127
    }
128
129
    /**
130
     * Adds a job to the queue to be started
131
     *
132
     * Relevant data about the job will be persisted using a QueuedJobDescriptor
133
     *
134
     * @param QueuedJob $job
135
     *          The job to start.
136
     * @param $startAfter
137
     *          The date (in Y-m-d H:i:s format) to start execution after
138
     * @param int $userId
139
     *          The ID of a user to execute the job as. Defaults to the current user
140
     * @return int
141
     */
142
    public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null)
143
    {
144
145
        $signature = $job->getSignature();
146
147
        // see if we already have this job in a queue
148
        $filter = array(
149
            'Signature' => $signature,
150
            'JobStatus' => array(
151
                QueuedJob::STATUS_NEW,
152
                QueuedJob::STATUS_INIT
153
            )
154
        );
155
156
        $existing = DataList::create('SilverStripe\\QueuedJobs\\DataObjects\\QueuedJobDescriptor')
157
            ->filter($filter)
158
            ->first();
159
160
        if ($existing && $existing->ID) {
161
            return $existing->ID;
162
        }
163
164
        $jobDescriptor = new QueuedJobDescriptor();
165
        $jobDescriptor->JobTitle = $job->getTitle();
166
        $jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
167
        $jobDescriptor->Signature = $signature;
168
        $jobDescriptor->Implementation = get_class($job);
169
        $jobDescriptor->StartAfter = $startAfter;
170
171
        $jobDescriptor->RunAsID = $userId ? $userId : Member::currentUserID();
0 ignored issues
show
Documentation introduced by
The property RunAsID does not exist on object<SilverStripe\Queu...ts\QueuedJobDescriptor>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
172
173
        // copy data
174
        $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<SilverStripe\Queu...ts\QueuedJobDescriptor>, but the function expects a object<SilverStripe\Queu...Services\JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
175
176
        $jobDescriptor->write();
177
178
        $this->startJob($jobDescriptor, $startAfter);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<SilverStripe\Queu...ts\QueuedJobDescriptor>, but the function expects a object<SilverStripe\Queu...Services\JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
179
180
        return $jobDescriptor->ID;
181
    }
182
183
    /**
184
     * Start a job (or however the queue handler determines it should be started)
185
     *
186
     * @param JobDescriptor $jobDescriptor
187
     * @param date $startAfter
188
     */
189
    public function startJob($jobDescriptor, $startAfter = null)
190
    {
191
        if ($startAfter && strtotime($startAfter) > time()) {
192
            $this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
193
        } else {
194
            // immediately start it on the queue, however that works
195
            $this->queueHandler->startJobOnQueue($jobDescriptor);
196
        }
197
    }
198
199
    /**
200
     * Copies data from a job into a descriptor for persisting
201
     *
202
     * @param QueuedJob $job
203
     * @param JobDescriptor $jobDescriptor
204
     */
205
    protected function copyJobToDescriptor($job, $jobDescriptor)
206
    {
207
        $data = $job->getJobData();
208
209
        $jobDescriptor->TotalSteps = $data->totalSteps;
210
        $jobDescriptor->StepsProcessed = $data->currentStep;
211
        if ($data->isComplete) {
212
            $jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
213
            $jobDescriptor->JobFinished = date('Y-m-d H:i:s');
214
        }
215
216
        $jobDescriptor->SavedJobData = serialize($data->jobData);
217
        $jobDescriptor->SavedJobMessages = serialize($data->messages);
218
    }
219
220
    /**
221
     * @param QueuedJobDescriptor $jobDescriptor
222
     * @param QueuedJob $job
223
     */
224
    protected function copyDescriptorToJob($jobDescriptor, $job)
225
    {
226
        $jobData = null;
0 ignored issues
show
Unused Code introduced by
$jobData is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
227
        $messages = null;
0 ignored issues
show
Unused Code introduced by
$messages is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
228
229
        // switching to php's serialize methods... not sure why this wasn't done from the start!
230
        $jobData = @unserialize($jobDescriptor->SavedJobData);
231
        $messages = @unserialize($jobDescriptor->SavedJobMessages);
232
233
        if (!$jobData) {
234
            // SS's convert:: function doesn't do this detection for us!!
235
            if (function_exists('json_decode')) {
236
                $jobData = json_decode($jobDescriptor->SavedJobData);
237
                $messages = json_decode($jobDescriptor->SavedJobMessages);
238
            } else {
239
                $jobData = Convert::json2obj($jobDescriptor->SavedJobData);
240
                $messages = Convert::json2obj($jobDescriptor->SavedJobMessages);
241
            }
242
        }
243
244
        $job->setJobData(
245
            $jobDescriptor->TotalSteps,
246
            $jobDescriptor->StepsProcessed,
247
            $jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
248
            $jobData,
249
            $messages
250
        );
251
    }
252
253
    /**
254
     * Check the current job queues and see if any of the jobs currently in there should be started. If so,
255
     * return the next job that should be executed
256
     *
257
     * @param string $type Job type
258
     * @return QueuedJobDescriptor
259
     */
260
    public function getNextPendingJob($type = null)
261
    {
262
        // Filter jobs by type
263
        $type = $type ?: QueuedJob::QUEUED;
264
        $list = QueuedJobDescriptor::get()
265
            ->filter('JobType', $type)
266
            ->sort('ID', 'ASC');
267
268
        // see if there's any blocked jobs that need to be resumed
269
        $waitingJob = $list
270
            ->filter('JobStatus', QueuedJob::STATUS_WAIT)
271
            ->first();
272
        if ($waitingJob) {
273
            return $waitingJob;
274
        }
275
276
        // If there's an existing job either running or pending, the lets just return false to indicate
277
        // that we're still executing
278
        $runningJob = $list
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $runningJob is correct as $list->filter('JobStatus...::STATUS_RUN))->first() (which targets SilverStripe\ORM\DataList::first()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
279
            ->filter('JobStatus', array(QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN))
280
            ->first();
281
        if ($runningJob) {
282
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\QueuedJobs\...vice::getNextPendingJob of type SilverStripe\QueuedJobs\...ueuedJobDescriptor|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
283
        }
284
285
        // Otherwise, lets find any 'new' jobs that are waiting to execute
286
        $newJob = $list
287
            ->filter('JobStatus', QueuedJob::STATUS_NEW)
288
            ->where(sprintf(
289
                '"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
290
                DBDatetime::now()->getValue()
291
            ))
292
            ->first();
293
294
        return $newJob;
295
    }
296
297
    /**
298
     * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
299
     * between each run. If it's not, then we need to flag it as paused due to an error.
300
     *
301
     * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
302
     * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
303
     * fix them
304
     *
305
     * @param int $queue The queue to check against
306
     */
307
    public function checkJobHealth($queue = null)
308
    {
309
        $queue = $queue ?: QueuedJob::QUEUED;
310
        // Select all jobs currently marked as running
311
        $runningJobs = QueuedJobDescriptor::get()
312
            ->filter(array(
313
                'JobStatus' => array(
314
                    QueuedJob::STATUS_RUN,
315
                    QueuedJob::STATUS_INIT,
316
                ),
317
                'JobType' => $queue,
318
            ));
319
320
        // If no steps have been processed since the last run, consider it a broken job
321
        // Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
322
        $stalledJobs = $runningJobs
323
            ->filter('LastProcessedCount:GreaterThanOrEqual', 0)
324
            ->where('"StepsProcessed" = "LastProcessedCount"');
325
        foreach ($stalledJobs as $stalledJob) {
326
            $this->restartStalledJob($stalledJob);
327
        }
328
329
        // now, find those that need to be marked before the next check
330
        // foreach job, mark it as having been incremented
331
        foreach ($runningJobs as $job) {
332
            $job->LastProcessedCount = $job->StepsProcessed;
333
            $job->write();
334
        }
335
336
        // finally, find the list of broken jobs and send an email if there's some found
337
        $brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
338 View Code Duplication
        if ($brokenJobs && $brokenJobs->count()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
339
            $this->getLogger()->error(
340
                print_r(
341
                    array(
342
                        'errno' => 0,
343
                        'errstr' => 'Broken jobs were found in the job queue',
344
                        'errfile' => __FILE__,
345
                        'errline' => __LINE__,
346
                        'errcontext' => array()
347
                    ),
348
                    true
349
                )
350
            );
351
        }
352
    }
353
354
    /**
355
     * Attempt to restart a stalled job
356
     *
357
     * @param QueuedJobDescriptor $stalledJob
358
     * @return bool True if the job was successfully restarted
359
     */
360
    protected function restartStalledJob($stalledJob)
361
    {
362
        if ($stalledJob->ResumeCounts < Config::inst()->get(__CLASS__, 'stall_threshold')) {
363
            $stalledJob->restart();
364
            $message = sprintf(
365
                _t(
366
                    'QueuedJobs.STALLED_JOB_MSG',
367
                    'A job named %s appears to have stalled. It will be stopped and restarted, please login to make sure it has continued'
368
                ),
369
                $stalledJob->JobTitle
370
            );
371
        } else {
372
            $stalledJob->pause();
373
            $message = sprintf(
374
                _t(
375
                    'QueuedJobs.STALLED_JOB_MSG',
376
                    'A job named %s appears to have stalled. It has been paused, please login to check it'
377
                ),
378
                $stalledJob->JobTitle
379
            );
380
        }
381
382
        singleton('SilverStripe\\QueuedJobs\\QJUtils')->log($message);
383
        $from = Config::inst()->get('SilverStripe\\Control\\Email\\Email', 'admin_email');
384
        $to = Config::inst()->get('SilverStripe\\Control\\Email\\Email', 'queued_job_admin_email');
385
        $subject = _t('QueuedJobs.STALLED_JOB', 'Stalled job');
386
        $mail = new Email($from, $to, $subject, $message);
387
        $mail->send();
388
    }
389
390
    /**
391
     * Prepares the given jobDescriptor for execution. Returns the job that
392
     * will actually be run in a state ready for executing.
393
     *
394
     * Note that this is called each time a job is picked up to be executed from the cron
395
     * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
396
     * so your job MUST detect that and act accordingly.
397
     *
398
     * @param QueuedJobDescriptor $jobDescriptor
399
     *          The Job descriptor of a job to prepare for execution
400
     *
401
     * @return QueuedJob|boolean
402
     */
403
    protected function initialiseJob(QueuedJobDescriptor $jobDescriptor)
404
    {
405
        // create the job class
406
        $impl = $jobDescriptor->Implementation;
407
        $job = Object::create($impl);
408
        /* @var $job QueuedJob */
409
        if (!$job) {
410
            throw new Exception("Implementation $impl no longer exists");
411
        }
412
413
        $jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
414
        $jobDescriptor->write();
415
416
        // make sure the data is there
417
        $this->copyDescriptorToJob($jobDescriptor, $job);
418
419
        // see if it needs 'setup' or 'restart' called
420
        if ($jobDescriptor->StepsProcessed <= 0) {
421
            $job->setup();
422
        } else {
423
            $job->prepareForRestart();
424
        }
425
426
        // make sure the descriptor is up to date with anything changed
427
        $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<SilverStripe\Queu...ts\QueuedJobDescriptor>, but the function expects a object<SilverStripe\Queu...Services\JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
428
        $jobDescriptor->write();
429
430
        return $job;
431
    }
432
433
    /**
434
     * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
435
     * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
436
     * This may still have a race condition, but this should minimise the possibility.
437
     * Side effect is the job status will be changed to "Initialised".
438
     *
439
     * Assumption is the job has a status of "Queued" or "Wait".
440
     *
441
     * @param QueuedJobDescriptor $jobDescriptor
442
     * @return boolean
443
     */
444
    protected function grabMutex(QueuedJobDescriptor $jobDescriptor)
445
    {
446
        // write the status and determine if any rows were affected, for protection against a
447
        // potential race condition where two or more processes init the same job at once.
448
        // This deliberately does not use write() as that would always update LastEdited
449
        // and thus the row would always be affected.
450
        try {
451
            DB::query(sprintf(
452
                'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s',
453
                QueuedJob::STATUS_INIT,
454
                $jobDescriptor->ID
455
            ));
456
        } catch (Exception $e) {
457
            return false;
458
        }
459
460
        if (DB::getConn()->affectedRows() === 0 && $jobDescriptor->JobStatus !== QueuedJob::STATUS_INIT) {
0 ignored issues
show
Deprecated Code introduced by
The method SilverStripe\ORM\DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
461
            return false;
462
        }
463
464
        return true;
465
    }
466
467
    /**
468
     * Start the actual execution of a job.
469
     * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
470
     *
471
     * This method will continue executing until the job says it's completed
472
     *
473
     * @param int $jobId
474
     *          The ID of the job to start executing
475
     * @return boolean
476
     */
477
    public function runJob($jobId)
0 ignored issues
show
Coding Style introduced by
runJob uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
478
    {
479
        // first retrieve the descriptor
480
        $jobDescriptor = DataObject::get_by_id(
481
            'SilverStripe\\QueuedJobs\\DataObjects\\QueuedJobDescriptor',
482
            (int) $jobId
483
        );
484
        if (!$jobDescriptor) {
485
            throw new Exception("$jobId is invalid");
486
        }
487
488
        // now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
489
        // we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
490
        // want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
491
        // as the currently logged in user and hope for the best
492
493
        // We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
494
        // this point of execution in some circumstances
495
        $originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
496
        $originalUser = $originalUserID
497
            ? DataObject::get_by_id('SilverStripe\\Security\\Member', $originalUserID)
498
            : null;
499
        $runAsUser = null;
500
501
        if (Director::is_cli() || !$originalUser || Permission::checkMember($originalUser, 'ADMIN')) {
502
            $runAsUser = $jobDescriptor->RunAs();
503
            if ($runAsUser && $runAsUser->exists()) {
504
                // the job runner outputs content way early in the piece, meaning there'll be cookie errors
505
                // if we try and do a normal login, and we only want it temporarily...
506
                if (Controller::has_curr()) {
507
                    Session::set('loggedInAs', $runAsUser->ID);
508
                } else {
509
                    $_SESSION['loggedInAs'] = $runAsUser->ID;
510
                }
511
512
                // this is an explicit coupling brought about by SS not having
513
                // a nice way of mocking a user, as it requires session
514
                // nastiness
515
                if (class_exists('SecurityContext')) {
516
                    singleton('SecurityContext')->setMember($runAsUser);
517
                }
518
            }
519
        }
520
521
        // set up a custom error handler for this processing
522
        $errorHandler = new JobErrorHandler();
523
524
        $job = null;
525
526
        $broken = false;
527
528
        // Push a config context onto the stack for the duration of this job run.
529
        Config::nest();
530
531
        if ($this->grabMutex($jobDescriptor)) {
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<SilverStripe\ORM\DataObject> is not a sub-type of object<SilverStripe\Queu...ts\QueuedJobDescriptor>. It seems like you assume a child class of the class SilverStripe\ORM\DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
532
            try {
533
                $job = $this->initialiseJob($jobDescriptor);
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<SilverStripe\ORM\DataObject> is not a sub-type of object<SilverStripe\Queu...ts\QueuedJobDescriptor>. It seems like you assume a child class of the class SilverStripe\ORM\DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
534
535
                // get the job ready to begin.
536
                if (!$jobDescriptor->JobStarted) {
537
                    $jobDescriptor->JobStarted = date('Y-m-d H:i:s');
538
                } else {
539
                    $jobDescriptor->JobRestarted = date('Y-m-d H:i:s');
540
                }
541
542
                $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
543
                $jobDescriptor->write();
544
545
                $lastStepProcessed = 0;
546
                // have we stalled at all?
547
                $stallCount = 0;
548
549
                if ($job->SubsiteID && class_exists('Subsite')) {
550
                    /**
551
                     * @todo Check for 4.x compatibility with Subsites once namespacing is implemented
552
                     */
553
                    \Subsite::changeSubsite($job->SubsiteID);
554
555
                    // lets set the base URL as far as Director is concerned so that our URLs are correct
556
                    $subsite = DataObject::get_by_id('Subsite', $job->SubsiteID);
557
                    if ($subsite && $subsite->exists()) {
558
                        $domain = $subsite->domain();
559
                        $base = rtrim(Director::protocol() . $domain, '/') . '/';
560
561
                        Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', $base);
562
                    }
563
                }
564
565
                // while not finished
566
                while (!$job->jobFinished() && !$broken) {
567
                    // see that we haven't been set to 'paused' or otherwise by another process
568
                    $jobDescriptor = DataObject::get_by_id(
569
                        'SilverStripe\\QueuedJobs\\DataObjects\\QueuedJobDescriptor',
570
                        (int) $jobId
571
                    );
572
                    if (!$jobDescriptor || !$jobDescriptor->exists()) {
573
                        $broken = true;
574
                        $this->getLogger()->error(
575
                            print_r(
576
                                array(
577
                                    'errno' => 0,
578
                                    'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
579
                                    'errfile' => __FILE__,
580
                                    'errline' => __LINE__,
581
                                    'errcontext' => array()
582
                                ),
583
                                true
584
                            )
585
                        );
586
                        break;
587
                    }
588
                    if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
589
                        // we've been paused by something, so we'll just exit
590
                        $job->addMessage(sprintf(_t('QueuedJobs.JOB_PAUSED', "Job paused at %s"), date('Y-m-d H:i:s')));
591
                        $broken = true;
592
                    }
593
594
                    if (!$broken) {
595
                        try {
596
                            $job->process();
597
                        } catch (Exception $e) {
598
                            // okay, we'll just catch this exception for now
599
                            $job->addMessage(
600
                                sprintf(
601
                                    _t('QueuedJobs.JOB_EXCEPT', 'Job caused exception %s in %s at line %s'),
602
                                    $e->getMessage(),
603
                                    $e->getFile(),
604
                                    $e->getLine()
605
                                ),
606
                                'ERROR'
0 ignored issues
show
Unused Code introduced by
The call to QueuedJob::addMessage() has too many arguments starting with 'ERROR'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
607
                            );
608
                            $this->getLogger()->error($e->getMessage());
609
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
610
                        }
611
612
                        // now check the job state
613
                        $data = $job->getJobData();
614
                        if ($data->currentStep == $lastStepProcessed) {
615
                            $stallCount++;
616
                        }
617
618
                        if ($stallCount > Config::inst()->get(__CLASS__, 'stall_threshold')) {
619
                            $broken = true;
620
                            $job->addMessage(
621
                                sprintf(
622
                                    _t('QueuedJobs.JOB_STALLED', "Job stalled after %s attempts - please check"),
623
                                    $stallCount
624
                                ),
625
                                'ERROR'
0 ignored issues
show
Unused Code introduced by
The call to QueuedJob::addMessage() has too many arguments starting with 'ERROR'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
626
                            );
627
                            $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
628
                        }
629
630
                        // now we'll be good and check our memory usage. If it is too high, we'll set the job to
631
                        // a 'Waiting' state, and let the next processing run pick up the job.
632
                        if ($this->isMemoryTooHigh()) {
633
                            $job->addMessage(sprintf(
634
                                _t('QueuedJobs.MEMORY_RELEASE', 'Job releasing memory and waiting (%s used)'),
635
                                $this->humanReadable($this->getMemoryUsage())
636
                            ));
637
                            $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
638
                            $broken = true;
639
                        }
640
641
                        // Also check if we are running too long
642
                        if ($this->hasPassedTimeLimit()) {
643
                            $job->addMessage(_t(
644
                                'QueuedJobs.TIME_LIMIT',
645
                                'Queue has passed time limit and will restart before continuing'
646
                            ));
647
                            $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
648
                            $broken = true;
649
                        }
650
                    }
651
652
                    if ($jobDescriptor) {
653
                        $this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Bug introduced by
It seems like $job defined by $this->initialiseJob($jobDescriptor) on line 533 can also be of type boolean; however, SilverStripe\QueuedJobs\...::copyJobToDescriptor() does only seem to accept object<SilverStripe\Queu...obs\Services\QueuedJob>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Documentation introduced by
$jobDescriptor is of type object<SilverStripe\ORM\DataObject>, but the function expects a object<SilverStripe\Queu...Services\JobDescriptor>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
654
                        $jobDescriptor->write();
655 View Code Duplication
                    } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
656
                        $this->getLogger()->warn(
657
                            print_r(
658
                                array(
659
                                    'errno' => 0,
660
                                    'errstr' => 'Job descriptor has been set to null',
661
                                    'errfile' => __FILE__,
662
                                    'errline' => __LINE__,
663
                                    'errcontext' => array()
664
                                ),
665
                                true
666
                            )
667
                        );
668
                        $broken = true;
669
                    }
670
                }
671
672
                // a last final save. The job is complete by now
673
                if ($jobDescriptor) {
674
                    $jobDescriptor->write();
675
                }
676
677
                if (!$broken) {
678
                    $job->afterComplete();
679
                    $jobDescriptor->cleanupJob();
680
                }
681
            } catch (Exception $e) {
682
                // okay, we'll just catch this exception for now
683
                $this->getLogger()->error($e->getMessage());
684
                $jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
685
                $jobDescriptor->write();
686
                $broken = true;
687
            }
688
        }
689
690
        $errorHandler->clear();
691
692
        Config::unnest();
693
694
        // okay let's reset our user if we've got an original
695
        if ($runAsUser && $originalUser) {
696
            Session::clear("loggedInAs");
697
            if ($originalUser) {
698
                Session::set("loggedInAs", $originalUser->ID);
699
            }
700
        }
701
702
        return !$broken;
703
    }
704
705
    /**
706
     * Start timer
707
     */
708
    protected function markStarted()
709
    {
710
        if ($this->startedAt) {
711
            $this->startedAt = DBDatetime::now()->Format('U');
0 ignored issues
show
Documentation Bug introduced by
It seems like \SilverStripe\ORM\FieldT...ime::now()->Format('U') can also be of type string. However, the property $startedAt is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
712
        }
713
    }
714
715
    /**
716
     * Is execution time too long?
717
     *
718
     * @return bool True if the script has passed the configured time_limit
719
     */
720
    protected function hasPassedTimeLimit()
721
    {
722
        // Ensure a limit exists
723
        $limit = Config::inst()->get(__CLASS__, 'time_limit');
724
        if (!$limit) {
725
            return false;
726
        }
727
728
        // Ensure started date is set
729
        $this->markStarted();
730
731
        // Check duration
732
        $now = DBDatetime::now()->Format('U');
733
        return $now > $this->startedAt + $limit;
734
    }
735
736
    /**
737
     * Is memory usage too high?
738
     *
739
     * @return bool
740
     */
741
    protected function isMemoryTooHigh()
742
    {
743
        $used = $this->getMemoryUsage();
744
        $limit = $this->getMemoryLimit();
745
        return $limit && ($used > $limit);
746
    }
747
748
    /**
749
     * Get peak memory usage of this application
750
     *
751
     * @return float
752
     */
753
    protected function getMemoryUsage()
754
    {
755
        // Note we use real_usage = false
756
        // http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
757
        // Also we use the safer peak memory usage
758
        return (float)memory_get_peak_usage(false);
759
    }
760
761
    /**
762
     * Determines the memory limit (in bytes) for this application
763
     * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
764
     *
765
     * @return float Memory limit in bytes
766
     */
767
    protected function getMemoryLimit()
768
    {
769
        // Limit to smaller of explicit limit or php memory limit
770
        $limit = $this->parseMemory(Config::inst()->get(__CLASS__, 'memory_limit'));
771
        if ($limit) {
772
            return $limit;
773
        }
774
775
        // Fallback to php memory limit
776
        $phpLimit = $this->getPHPMemoryLimit();
777
        if ($phpLimit) {
778
            return $phpLimit;
779
        }
780
    }
781
782
    /**
783
     * Calculate the current memory limit of the server
784
     *
785
     * @return float
786
     */
787
    protected function getPHPMemoryLimit()
788
    {
789
        return $this->parseMemory(trim(ini_get("memory_limit")));
790
    }
791
792
    /**
793
     * Convert memory limit string to bytes.
794
     * Based on implementation in install.php5
795
     *
796
     * @param string $memString
797
     * @return float
798
     */
799
    protected function parseMemory($memString)
800
    {
801
        switch (strtolower(substr($memString, -1))) {
802
            case "b":
803
                return round(substr($memString, 0, -1));
804
            case "k":
805
                return round(substr($memString, 0, -1) * 1024);
806
            case "m":
807
                return round(substr($memString, 0, -1) * 1024 * 1024);
808
            case "g":
809
                return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
810
            default:
811
                return round($memString);
812
        }
813
    }
814
815
    protected function humanReadable($size)
816
    {
817
        $filesizename = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
818
        return $size ? round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
819
    }
820
821
822
    /**
823
     * Gets a list of all the current jobs (or jobs that have recently finished)
824
     *
825
     * @param string $type
826
     *          if we're after a particular job list
827
     * @param int $includeUpUntil
828
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
829
     *          includes recently finished jobs
830
     * @return QueuedJobDescriptor
831
     */
832
    public function getJobList($type = null, $includeUpUntil = 0)
833
    {
834
        return DataObject::get(
835
            'SilverStripe\\QueuedJobs\\DataObjects\\QueuedJobDescriptor',
836
            $this->getJobListFilter($type, $includeUpUntil)
837
        );
838
    }
839
840
    /**
841
     * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
842
     *
843
     * @param string $type
844
     *          if we're after a particular job list
845
     * @param int $includeUpUntil
846
     *          The number of seconds to include jobs that have just finished, allowing a job list to be built that
847
     *          includes recently finished jobs
848
     * @return string
849
     */
850
    public function getJobListFilter($type = null, $includeUpUntil = 0)
851
    {
852
        $util = singleton('SilverStripe\\QueuedJobs\\QJUtils');
853
854
        $filter = array('JobStatus <>' => QueuedJob::STATUS_COMPLETE);
855
        if ($includeUpUntil) {
856
            $filter['JobFinished > '] = date('Y-m-d H:i:s', time() - $includeUpUntil);
857
        }
858
859
        $filter = $util->dbQuote($filter, ' OR ');
860
861
        if ($type) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $type of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
862
            $filter = $util->dbQuote(array('JobType =' => (string) $type)). ' AND ('.$filter.')';
863
        }
864
865
        return $filter;
866
    }
867
868
    /**
869
     * Process the job queue with the current queue runner
870
     *
871
     * @param string $queue
872
     */
873
    public function runQueue($queue)
874
    {
875
        $this->checkJobHealth($queue);
876
        $this->queueRunner->runQueue($queue);
877
    }
878
879
    /**
880
     * Process all jobs from a given queue
881
     *
882
     * @param string $name The job queue to completely process
883
     */
884
    public function processJobQueue($name)
0 ignored issues
show
Coding Style introduced by
processJobQueue uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
885
    {
886
        // Start timer to measure lifetime
887
        $this->markStarted();
888
889
        // Begin main loop
890
        do {
891
            if (class_exists('Subsite')) {
892
                // clear subsite back to default to prevent any subsite changes from leaking to
893
                // subsequent actions
894
                /**
895
                 * @todo Check for 4.x compatibility with Subsites once namespacing is implemented
896
                 */
897
                \Subsite::changeSubsite(0);
898
            }
899
            if (Controller::has_curr()) {
900
                Session::clear('loggedInAs');
901
            } else {
902
                unset($_SESSION['loggedInAs']);
903
            }
904
905
            if (class_exists('SecurityContext')) {
906
                singleton('SecurityContext')->setMember(null);
907
            }
908
909
            $job = $this->getNextPendingJob($name);
910
            if ($job) {
911
                $success = $this->runJob($job->ID);
912
                if (!$success) {
913
                    // make sure job is null so it doesn't continue the current
914
                    // processing loop. Next queue executor can pick up where
915
                    // things left off
916
                    $job = null;
917
                }
918
            }
919
        } while ($job);
920
    }
921
922
    /**
923
     * When PHP shuts down, we want to process all of the immediate queue items
924
     *
925
     * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
926
     * we ignore paused or stalled jobs.
927
     */
928
    public function onShutdown()
929
    {
930
        $this->processJobQueue(QueuedJob::IMMEDIATE);
931
    }
932
933
    /**
934
     * Get a logger
935
     * @return LoggerInterface
936
     */
937
    public function getLogger()
938
    {
939
        return Injector::inst()->get('Logger');
940
    }
941
}
942
943
/**
944
 * Class used to handle errors for a single job
945
 */
946
class JobErrorHandler
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
947
{
948
    public function __construct()
949
    {
950
        set_error_handler(array($this, 'handleError'));
951
    }
952
953
    public function clear()
954
    {
955
        restore_error_handler();
956
    }
957
958
    public function handleError($errno, $errstr, $errfile, $errline)
959
    {
960
        if (error_reporting()) {
961
            // Don't throw E_DEPRECATED in PHP 5.3+
962
            if (defined('E_DEPRECATED')) {
963
                if ($errno == E_DEPRECATED || $errno = E_USER_DEPRECATED) {
964
                    return;
965
                }
966
            }
967
968
            switch ($errno) {
969
                case E_NOTICE:
970
                case E_USER_NOTICE:
971
                case E_STRICT:
972
                    break;
973
                default:
974
                    throw new Exception($errstr . " in $errfile at line $errline", $errno);
975
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
976
            }
977
        }
978
    }
979
}
980