Completed
Pull Request — 2.10 (#130)
by Fred
02:54
created

JobErrorHandler::handleException()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 1
1
<?php
2
3
/**
4
 * A service that can be used for starting, stopping and listing queued jobs.
5
 *
6
 * When a job is first added, it is initialised, its job type determined, then persisted to the database
7
 *
8
 * When the queues are scanned, a job is reloaded and processed. Ignoring the persistence and reloading, it looks
9
 * something like
10
 *
11
 * job->getJobType();
12
 * job->getJobData();
13
 * data->write();
14
 * job->setup();
15
 * while !job->isComplete
16
 *	job->process();
17
 *	job->getJobData();
18
 *  data->write();
19
 *
20
 *
21
 * @author Marcus Nyeholt <[email protected]>
22
 * @license BSD http://silverstripe.org/bsd-license/
23
 */
24
class QueuedJobService {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
25
	/**
26
	 * @var int
27
	 */
28
	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...
29
30
	/**
31
	 * How much ram will we allow before pausing and releasing the memory?
32
	 *
33
	 * For instance, set to 134217728 (128MB) to pause this process if used memory exceeds
34
	 * this value. This needs to be set to a value lower than the php_ini max_memory as
35
	 * the system will otherwise crash before shutdown can be handled gracefully.
36
	 *
37
	 * @var int
38
	 * @config
39
	 */
40
	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...
41
42
	/**
43
	 * Optional time limit (in seconds) to run the service before restarting to release resources.
44
	 *
45
	 * Defaults to no limit.
46
	 *
47
	 * @var int
48
	 * @config
49
	 */
50
	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...
51
52
	/**
53
	 * Timestamp (in seconds) when the queue was started
54
	 *
55
	 * @var int
56
	 */
57
	protected $startedAt = 0;
58
59
	/**
60
	 * Should "immediate" jobs be managed using the shutdown function?
61
	 *
62
	 * It is recommended you set up an inotify watch and use that for
63
	 * triggering immediate jobs. See the wiki for more information
64
	 *
65
	 * @var boolean
66
	 */
67
	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...
68
69
	/**
70
	 * The location for immediate jobs to be stored in
71
	 *
72
	 * @var string
73
	 */
74
	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...
75
76
	/**
77
	 * @var DefaultQueueHandler
78
	 */
79
	public $queueHandler;
80
81
	/**
82
	 *
83
	 * @var TaskRunnerEngine
84
	 */
85
	public $queueRunner;
86
87
	/**
88
	 * Config controlled list of default/required jobs
89
	 * @var Array
90
	 */
91
	public $defaultJobs;
92
93
	/**
94
	 * Register our shutdown handler
95
	 */
96
	public function __construct() {
97
		// bind a shutdown function to process all 'immediate' queued jobs if needed, but only in CLI mode
98
		if (Config::inst()->get(__CLASS__, 'use_shutdown_function') && Director::is_cli()) {
99
			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...
100
				// do NOTHING
101
			} else {
102
				register_shutdown_function(array($this, 'onShutdown'));
103
			}
104
105
		}
106
		if (Config::inst()->get('Email', 'queued_job_admin_email') == '') {
107
			Config::inst()->update('Email', 'queued_job_admin_email', Config::inst()->get('Email', 'admin_email'));
108
		}
109
	}
110
111
	/**
112
	 * Adds a job to the queue to be started
113
	 *
114
	 * Relevant data about the job will be persisted using a QueuedJobDescriptor
115
	 *
116
	 * @param QueuedJob $job
117
	 *			The job to start.
118
	 * @param $startAfter
119
	 *			The date (in Y-m-d H:i:s format) to start execution after
120
	 * @param int $userId
121
	 *			The ID of a user to execute the job as. Defaults to the current user
122
	 * @return int
123
	 */
124
	public function queueJob(QueuedJob $job, $startAfter = null, $userId = null, $queueName = null) {
125
126
		$signature = $job->getSignature();
127
128
		// see if we already have this job in a queue
129
		$filter = array(
130
			'Signature' => $signature,
131
			'JobStatus' => array(
132
				QueuedJob::STATUS_NEW,
133
				QueuedJob::STATUS_INIT
134
			)
135
		);
136
137
		$existing = DataList::create('QueuedJobDescriptor')->filter($filter)->first();
138
139
		if ($existing && $existing->ID) {
140
			return $existing->ID;
141
		}
142
143
		$jobDescriptor = new QueuedJobDescriptor();
144
		$jobDescriptor->JobTitle = $job->getTitle();
145
		$jobDescriptor->JobType = $queueName ? $queueName : $job->getJobType();
146
		$jobDescriptor->Signature = $signature;
147
		$jobDescriptor->Implementation = get_class($job);
148
		$jobDescriptor->StartAfter = $startAfter;
149
150
		$jobDescriptor->RunAsID = $userId ? $userId : Member::currentUserID();
0 ignored issues
show
Documentation introduced by
The property RunAsID does not exist on object<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...
151
152
		// copy data
153
		$this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<QueuedJobDescriptor>, but the function expects a object<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...
154
155
		$jobDescriptor->write();
156
157
		$this->startJob($jobDescriptor, $startAfter);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<QueuedJobDescriptor>, but the function expects a object<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...
158
159
		return $jobDescriptor->ID;
160
	}
161
162
	/**
163
	 * Start a job (or however the queue handler determines it should be started)
164
	 *
165
	 * @param JobDescriptor $jobDescriptor
166
	 * @param date $startAfter
167
	 */
168
	public function startJob($jobDescriptor, $startAfter = null) {
169
		if ($startAfter && strtotime($startAfter) > time()) {
170
			$this->queueHandler->scheduleJob($jobDescriptor, $startAfter);
171
		} else {
172
			// immediately start it on the queue, however that works
173
			$this->queueHandler->startJobOnQueue($jobDescriptor);
174
		}
175
	}
176
177
	/**
178
	 * Copies data from a job into a descriptor for persisting
179
	 *
180
	 * @param QueuedJob $job
181
	 * @param JobDescriptor $jobDescriptor
182
	 */
183
	protected function copyJobToDescriptor($job, $jobDescriptor) {
184
		$data = $job->getJobData();
185
186
		$jobDescriptor->TotalSteps = $data->totalSteps;
187
		$jobDescriptor->StepsProcessed = $data->currentStep;
188
		if ($data->isComplete) {
189
			$jobDescriptor->JobStatus = QueuedJob::STATUS_COMPLETE;
190
			$jobDescriptor->JobFinished = date('Y-m-d H:i:s');
191
		}
192
193
		$jobDescriptor->SavedJobData = serialize($data->jobData);
194
		$jobDescriptor->SavedJobMessages = serialize($data->messages);
195
	}
196
197
	/**
198
	 * @param QueuedJobDescriptor $jobDescriptor
199
	 * @param QueuedJob $job
200
	 */
201
	protected function copyDescriptorToJob($jobDescriptor, $job) {
202
		$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...
203
		$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...
204
205
		// switching to php's serialize methods... not sure why this wasn't done from the start!
206
		$jobData = @unserialize($jobDescriptor->SavedJobData);
207
		$messages = @unserialize($jobDescriptor->SavedJobMessages);
208
209
		if (!$jobData) {
210
			// SS's convert:: function doesn't do this detection for us!!
211
			if (function_exists('json_decode')) {
212
				$jobData = json_decode($jobDescriptor->SavedJobData);
213
				$messages = json_decode($jobDescriptor->SavedJobMessages);
214
			} else {
215
				$jobData = Convert::json2obj($jobDescriptor->SavedJobData);
216
				$messages = Convert::json2obj($jobDescriptor->SavedJobMessages);
217
			}
218
		}
219
220
		$job->setJobData(
221
			$jobDescriptor->TotalSteps,
222
			$jobDescriptor->StepsProcessed,
223
			$jobDescriptor->JobStatus == QueuedJob::STATUS_COMPLETE,
224
			$jobData,
225
			$messages
226
		);
227
	}
228
229
	/**
230
	 * Check the current job queues and see if any of the jobs currently in there should be started. If so,
231
	 * return the next job that should be executed
232
	 *
233
	 * @param string $type Job type
234
	 * @return QueuedJobDescriptor
235
	 */
236
	public function getNextPendingJob($type = null) {
237
		// Filter jobs by type
238
		$type = $type ?: QueuedJob::QUEUED;
239
		$list = QueuedJobDescriptor::get()
240
			->filter('JobType', $type)
241
			->sort('ID', 'ASC');
242
243
		// see if there's any blocked jobs that need to be resumed
244
		$waitingJob = $list
245
			->filter('JobStatus', QueuedJob::STATUS_WAIT)
246
			->first();
247
		if ($waitingJob) {
248
			return $waitingJob;
249
		}
250
251
		// If there's an existing job either running or pending, the lets just return false to indicate
252
		// that we're still executing
253
		$runningJob = $list
254
			->filter('JobStatus', array(QueuedJob::STATUS_INIT, QueuedJob::STATUS_RUN))
255
			->first();
256
		if ($runningJob) {
257
			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 QueuedJobService::getNextPendingJob of type QueuedJobDescriptor.

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...
258
		}
259
260
		// Otherwise, lets find any 'new' jobs that are waiting to execute
261
		$newJob = $list
262
			->filter('JobStatus', QueuedJob::STATUS_NEW)
263
			->where(sprintf(
264
				'"StartAfter" < \'%s\' OR "StartAfter" IS NULL',
265
				SS_DateTime::now()->getValue()
266
			))
267
			->first();
268
269
		return $newJob;
270
	}
271
272
	/**
273
	 * Runs an explicit check on all currently running jobs to make sure their "processed" count is incrementing
274
	 * between each run. If it's not, then we need to flag it as paused due to an error.
275
	 *
276
	 * This typically happens when a PHP fatal error is thrown, which can't be picked up by the error
277
	 * handler or exception checker; in this case, we detect these stalled jobs later and fix (try) to
278
	 * fix them
279
     *
280
     * @param int $queue The queue to check against
281
	 */
282
	public function checkJobHealth($queue = null) {
283
        $queue = $queue ?: QueuedJob::QUEUED;
284
		// Select all jobs currently marked as running
285
		$runningJobs = QueuedJobDescriptor::get()
286
			->filter(array(
287
				'JobStatus' => array(
288
					QueuedJob::STATUS_RUN,
289
					QueuedJob::STATUS_INIT,
290
				),
291
                'JobType' => $queue,
292
			));
293
294
		// If no steps have been processed since the last run, consider it a broken job
295
		// Only check jobs that have been viewed before. LastProcessedCount defaults to -1 on new jobs.
296
		$stalledJobs = $runningJobs
297
			->filter('LastProcessedCount:GreaterThanOrEqual', 0)
298
			->where('"StepsProcessed" = "LastProcessedCount"');
299
		foreach ($stalledJobs as $stalledJob) {
300
			$this->restartStalledJob($stalledJob);
301
		}
302
303
		// now, find those that need to be marked before the next check
304
		// foreach job, mark it as having been incremented
305
		foreach ($runningJobs as $job) {
306
			$job->LastProcessedCount = $job->StepsProcessed;
307
			$job->write();
308
		}
309
310
		// finally, find the list of broken jobs and send an email if there's some found
311
		$brokenJobs = QueuedJobDescriptor::get()->filter('JobStatus', QueuedJob::STATUS_BROKEN);
312 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...
313
			SS_Log::log(array(
314
				'errno' => 0,
315
				'errstr' => 'Broken jobs were found in the job queue',
316
				'errfile' => __FILE__,
317
				'errline' => __LINE__,
318
				'errcontext' => array()
319
			), SS_Log::ERR);
320
		}
321
	}
322
323
	/**
324
	 * Checks through all the scheduled jobs that are expected to exist
325
	 */
326
	public function checkdefaultJobs($queue = null) {
327
		$queue = $queue ?: QueuedJob::QUEUED;
328
		if (count($this->defaultJobs)) {
329
330
			$activeJobs = QueuedJobDescriptor::get()->filter(array(
331
					'JobStatus' => array(
332
					QueuedJob::STATUS_NEW,
333
					QueuedJob::STATUS_INIT,
334
					QueuedJob::STATUS_RUN,
335
					QueuedJob::STATUS_WAIT,
336
					QueuedJob::STATUS_PAUSED
337
				),
338
				'JobType' => $queue
339
			));
340
341
			foreach ($this->defaultJobs as $title => $jobConfig) {
342
				if (!isset($jobConfig['filter']) || !isset($jobConfig['type'])) {
343
					SS_Log::log("Default Job config: $title incorrectly set up. Please check the readme for examples", SS_Log::ERR);
344
					continue;
345
				}
346
347
				$job = $activeJobs->filter(array_merge(
348
					array('Implementation' => $jobConfig['type']), $jobConfig['filter']
349
				));
350
351
				if (!$job->count()) {
352
					Email::create()
353
						->setTo(isset($jobConfig['email']) ? $jobConfig['email'] : Email::config()->admin_email)
354
						->setSubject('Default Job "' . $title . '" missing')
355
						->populateTemplate(array('Title' => $title, 'Site' => Director::absoluteBaseURL()))
356
						->populateTemplate($jobConfig)
357
						->setTemplate('QueuedJobsDefaultJob')
358
						->send();
359
360
					if (isset($jobConfig['recreate']) && $jobConfig['recreate']) {
361
						if (!isset($jobConfig['construct']) || !isset($jobConfig['startDateFormat']) || !isset($jobConfig['startTimeString'])) {
362
							SS_Log::log("Default Job config: $title incorrectly set up. Please check the readme for examples", SS_Log::ERR);
363
							continue;
364
						}
365
						singleton('QueuedJobService')->queueJob(
366
							Injector::inst()->createWithArgs($jobConfig['type'], $jobConfig['construct']),
367
							date($jobConfig['startDateFormat'], strtotime($jobConfig['startTimeString']))
368
						);
369
					}
370
				}
371
			}
372
		}
373
	}
374
375
	/**
376
	 * Attempt to restart a stalled job
377
	 *
378
	 * @param QueuedJobDescriptor $stalledJob
379
	 * @return bool True if the job was successfully restarted
380
	 */
381
	protected function restartStalledJob($stalledJob) {
382
		if ($stalledJob->ResumeCounts < Config::inst()->get(__CLASS__, 'stall_threshold')) {
383
			$stalledJob->restart();
384
			$message = sprintf(
385
				_t(
386
					'QueuedJobs.STALLED_JOB_MSG',
387
					'A job named %s appears to have stalled. It will be stopped and restarted, please login to make sure it has continued'
388
				),
389
				$stalledJob->JobTitle
390
			);
391
		} else {
392
			$stalledJob->pause();
393
			$message = sprintf(
394
				_t(
395
					'QueuedJobs.STALLED_JOB_MSG',
396
					'A job named %s appears to have stalled. It has been paused, please login to check it'
397
				),
398
				$stalledJob->JobTitle
399
			);
400
		}
401
402
		singleton('QJUtils')->log($message);
403
		$from = Config::inst()->get('Email', 'admin_email');
404
		$to = Config::inst()->get('Email', 'queued_job_admin_email');
405
		$subject = _t('QueuedJobs.STALLED_JOB', 'Stalled job');
406
		$mail = new Email($from, $to, $subject, $message);
407
		$mail->send();
408
	}
409
410
	/**
411
	 * Prepares the given jobDescriptor for execution. Returns the job that
412
	 * will actually be run in a state ready for executing.
413
	 *
414
	 * Note that this is called each time a job is picked up to be executed from the cron
415
	 * job - meaning that jobs that are paused and restarted will have 'setup()' called on them again,
416
	 * so your job MUST detect that and act accordingly.
417
	 *
418
	 * @param QueuedJobDescriptor $jobDescriptor
419
	 *			The Job descriptor of a job to prepare for execution
420
	 *
421
	 * @return QueuedJob|boolean
422
	 */
423
	protected function initialiseJob(QueuedJobDescriptor $jobDescriptor) {
424
		// create the job class
425
		$impl = $jobDescriptor->Implementation;
426
		$job = Object::create($impl);
427
		/* @var $job QueuedJob */
428
		if (!$job) {
429
			throw new Exception("Implementation $impl no longer exists");
430
		}
431
432
		$jobDescriptor->JobStatus = QueuedJob::STATUS_INIT;
433
		$jobDescriptor->write();
434
435
		// make sure the data is there
436
		$this->copyDescriptorToJob($jobDescriptor, $job);
437
438
		// see if it needs 'setup' or 'restart' called
439
		if ($jobDescriptor->StepsProcessed <= 0) {
440
			$job->setup();
441
		} else {
442
			$job->prepareForRestart();
443
		}
444
445
		// make sure the descriptor is up to date with anything changed
446
		$this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Documentation introduced by
$jobDescriptor is of type object<QueuedJobDescriptor>, but the function expects a object<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...
447
		$jobDescriptor->write();
448
449
		return $job;
450
	}
451
452
	/**
453
	 * Given a {@link QueuedJobDescriptor} mark the job as initialised. Works sort of like a mutex.
454
	 * Currently a database lock isn't entirely achievable, due to database adapters not supporting locks.
455
	 * This may still have a race condition, but this should minimise the possibility.
456
	 * Side effect is the job status will be changed to "Initialised".
457
	 *
458
	 * Assumption is the job has a status of "Queued" or "Wait".
459
	 *
460
	 * @param QueuedJobDescriptor $jobDescriptor
461
	 * @return boolean
462
	 */
463
	protected function grabMutex(QueuedJobDescriptor $jobDescriptor) {
464
		// write the status and determine if any rows were affected, for protection against a
465
		// potential race condition where two or more processes init the same job at once.
466
		// This deliberately does not use write() as that would always update LastEdited
467
		// and thus the row would always be affected.
468
		try {
469
			DB::query(sprintf(
470
				'UPDATE "QueuedJobDescriptor" SET "JobStatus" = \'%s\' WHERE "ID" = %s',
471
				QueuedJob::STATUS_INIT,
472
				$jobDescriptor->ID
473
			));
474
		} catch(Exception $e) {
475
			return false;
476
		}
477
478
		if(DB::getConn()->affectedRows() === 0 && $jobDescriptor->JobStatus !== QueuedJob::STATUS_INIT) {
0 ignored issues
show
Deprecated Code introduced by
The method 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...
479
			return false;
480
		}
481
482
		return true;
483
	}
484
485
	/**
486
	 * Start the actual execution of a job.
487
	 * The assumption is the jobID refers to a {@link QueuedJobDescriptor} that is status set as "Queued".
488
	 *
489
	 * This method will continue executing until the job says it's completed
490
	 *
491
	 * @param int $jobId
492
	 *			The ID of the job to start executing
493
	 * @return boolean
494
	 */
495
	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...
496
		// first retrieve the descriptor
497
		$jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId);
498
		if (!$jobDescriptor) {
499
			throw new Exception("$jobId is invalid");
500
		}
501
502
		// now lets see whether we have a current user to run as. Typically, if the job is executing via the CLI,
503
		// we want it to actually execute as the RunAs user - however, if running via the web (which is rare...), we
504
		// want to ensure that the current user has admin privileges before switching. Otherwise, we just run it
505
		// as the currently logged in user and hope for the best
506
507
		// We need to use $_SESSION directly because SS ties the session to a controller that no longer exists at
508
		// this point of execution in some circumstances
509
		$originalUserID = isset($_SESSION['loggedInAs']) ? $_SESSION['loggedInAs'] : 0;
510
		$originalUser = $originalUserID ? DataObject::get_by_id('Member', $originalUserID) : null;
511
		$runAsUser = null;
512
513
		if (Director::is_cli() || !$originalUser || Permission::checkMember($originalUser, 'ADMIN')) {
514
			$runAsUser = $jobDescriptor->RunAs();
515
			if ($runAsUser && $runAsUser->exists()) {
516
				// the job runner outputs content way early in the piece, meaning there'll be cookie errors
517
				// if we try and do a normal login, and we only want it temporarily...
518
				if (Controller::has_curr()) {
519
					Session::set('loggedInAs', $runAsUser->ID);
520
				} else {
521
					$_SESSION['loggedInAs'] = $runAsUser->ID;
522
				}
523
524
				// this is an explicit coupling brought about by SS not having
525
				// a nice way of mocking a user, as it requires session
526
				// nastiness
527
				if (class_exists('SecurityContext')) {
528
					singleton('SecurityContext')->setMember($runAsUser);
529
				}
530
			}
531
		}
532
533
		// set up a custom error handler for this processing
534
		$errorHandler = new JobErrorHandler();
535
536
		$job = null;
537
538
		$broken = false;
539
540
		// Push a config context onto the stack for the duration of this job run.
541
		Config::nest();
542
543
		if($this->grabMutex($jobDescriptor)) {
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<DataObject> is not a sub-type of object<QueuedJobDescriptor>. It seems like you assume a child class of the class 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...
544
			try {
545
				$job = $this->initialiseJob($jobDescriptor);
0 ignored issues
show
Compatibility introduced by
$jobDescriptor of type object<DataObject> is not a sub-type of object<QueuedJobDescriptor>. It seems like you assume a child class of the class 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...
546
547
				// get the job ready to begin.
548
				if (!$jobDescriptor->JobStarted) {
549
					$jobDescriptor->JobStarted = date('Y-m-d H:i:s');
550
				} else {
551
					$jobDescriptor->JobRestarted = date('Y-m-d H:i:s');
552
				}
553
554
555
		                // Only write to job as "Running" if 'isComplete' was NOT set to true
556
       			        // during setup() or prepareForRestart()
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
557
		                if (!$job->jobFinished()) {
558
		                    $jobDescriptor->JobStatus = QueuedJob::STATUS_RUN;
559
		                    $jobDescriptor->write();
560
		                }
561
562
				$lastStepProcessed = 0;
563
				// have we stalled at all?
564
				$stallCount = 0;
565
566
				if ($job->SubsiteID && class_exists('Subsite')) {
567
					Subsite::changeSubsite($job->SubsiteID);
568
569
					// lets set the base URL as far as Director is concerned so that our URLs are correct
570
					$subsite = DataObject::get_by_id('Subsite', $job->SubsiteID);
571
					if ($subsite && $subsite->exists()) {
572
						$domain = $subsite->domain();
573
						$base = rtrim(Director::protocol() . $domain, '/') . '/';
574
575
						Config::inst()->update('Director', 'alternate_base_url', $base);
576
					}
577
				}
578
579
				// while not finished
580
				while (!$job->jobFinished() && !$broken) {
581
					// see that we haven't been set to 'paused' or otherwise by another process
582
					$jobDescriptor = DataObject::get_by_id('QueuedJobDescriptor', (int) $jobId);
583
					if (!$jobDescriptor || !$jobDescriptor->exists()) {
584
						$broken = true;
585
						SS_Log::log(array(
586
							'errno' => 0,
587
							'errstr' => 'Job descriptor ' . $jobId . ' could not be found',
588
							'errfile' => __FILE__,
589
							'errline' => __LINE__,
590
							'errcontext' => array()
591
						), SS_Log::ERR);
592
						break;
593
					}
594
					if ($jobDescriptor->JobStatus != QueuedJob::STATUS_RUN) {
595
						// we've been paused by something, so we'll just exit
596
						$job->addMessage(sprintf(_t('QueuedJobs.JOB_PAUSED', "Job paused at %s"), date('Y-m-d H:i:s')));
597
						$broken = true;
598
					}
599
600
					if (!$broken) {
601
						try {
602
							$job->process();
603
						} catch (Exception $e) {
604
							// okay, we'll just catch this exception for now
605
							$job->addMessage(sprintf(_t('QueuedJobs.JOB_EXCEPT', 'Job caused exception %s in %s at line %s'), $e->getMessage(), $e->getFile(), $e->getLine()), '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...
606
							$errorHandler->handleException($e);
607
							$jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
608
						}
609
610
						// now check the job state
611
						$data = $job->getJobData();
612
						if ($data->currentStep == $lastStepProcessed) {
613
							$stallCount++;
614
						}
615
616
						if ($stallCount > Config::inst()->get(__CLASS__, 'stall_threshold')) {
617
							$broken = true;
618
							$job->addMessage(sprintf(_t('QueuedJobs.JOB_STALLED', "Job stalled after %s attempts - please check"), $stallCount), '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...
619
							$jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
620
						}
621
622
						// now we'll be good and check our memory usage. If it is too high, we'll set the job to
623
						// a 'Waiting' state, and let the next processing run pick up the job.
624
						if ($this->isMemoryTooHigh()) {
625
							$job->addMessage(sprintf(
626
								_t('QueuedJobs.MEMORY_RELEASE', 'Job releasing memory and waiting (%s used)'),
627
								$this->humanReadable($this->getMemoryUsage())
628
							));
629
630
                            if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
631
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
632
                            }
633
634
							$broken = true;
635
						}
636
637
						// Also check if we are running too long
638
						if($this->hasPassedTimeLimit()) {
639
							$job->addMessage(_t(
640
								'QueuedJobs.TIME_LIMIT',
641
								'Queue has passed time limit and will restart before continuing'
642
							));
643
							if ($jobDescriptor->JobStatus != QueuedJob::STATUS_BROKEN) {
644
                                $jobDescriptor->JobStatus = QueuedJob::STATUS_WAIT;
645
                            }
646
							$broken = true;
647
						}
648
					}
649
650
					if ($jobDescriptor) {
651
						$this->copyJobToDescriptor($job, $jobDescriptor);
0 ignored issues
show
Bug introduced by
It seems like $job defined by $this->initialiseJob($jobDescriptor) on line 545 can also be of type boolean; however, QueuedJobService::copyJobToDescriptor() does only seem to accept object<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<DataObject>, but the function expects a object<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...
652
						$jobDescriptor->write();
653 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...
654
						SS_Log::log(array(
655
							'errno' => 0,
656
							'errstr' => 'Job descriptor has been set to null',
657
							'errfile' => __FILE__,
658
							'errline' => __LINE__,
659
							'errcontext' => array()
660
						), SS_Log::WARN);
661
						$broken = true;
662
					}
663
				}
664
665
				// a last final save. The job is complete by now
666
				if ($jobDescriptor) {
667
					$jobDescriptor->write();
668
				}
669
670
				if (!$broken) {
671
					$job->afterComplete();
672
					$jobDescriptor->cleanupJob();
673
				}
674
			} catch (Exception $e) {
675
				// okay, we'll just catch this exception for now
676
				$errorHandler->handleException($e);
677
				$jobDescriptor->JobStatus =  QueuedJob::STATUS_BROKEN;
678
				$jobDescriptor->write();
679
				$broken = true;
680
			}
681
		}
682
683
		$errorHandler->clear();
684
685
		Config::unnest();
686
687
		// okay let's reset our user if we've got an original
688
		if ($runAsUser && $originalUser) {
689
			Session::clear("loggedInAs");
690
			if ($originalUser) {
691
				Session::set("loggedInAs", $originalUser->ID);
692
			}
693
		}
694
695
		return !$broken;
696
	}
697
698
	/**
699
	 * Start timer
700
	 */
701
	protected function markStarted() {
702
		if($this->startedAt) {
703
			$this->startedAt = SS_Datetime::now()->Format('U');
0 ignored issues
show
Documentation Bug introduced by
It seems like \SS_Datetime::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...
704
		}
705
	}
706
707
	/**
708
	 * Is execution time too long?
709
	 *
710
	 * @return bool True if the script has passed the configured time_limit
711
	 */
712
	protected function hasPassedTimeLimit() {
713
		// Ensure a limit exists
714
		$limit = Config::inst()->get(__CLASS__, 'time_limit');
715
		if(!$limit) {
716
			return false;
717
		}
718
719
		// Ensure started date is set
720
		$this->markStarted();
721
722
		// Check duration
723
		$now = SS_Datetime::now()->Format('U');
724
		return $now > $this->startedAt + $limit;
725
	}
726
727
	/**
728
	 * Is memory usage too high?
729
	 *
730
	 * @return bool
731
	 */
732
	protected function isMemoryTooHigh() {
733
		$used = $this->getMemoryUsage();
734
		$limit = $this->getMemoryLimit();
735
		return $limit && ($used > $limit);
736
	}
737
738
	/**
739
	 * Get peak memory usage of this application
740
	 *
741
	 * @return float
742
	 */
743
	protected function getMemoryUsage() {
744
		// Note we use real_usage = false http://stackoverflow.com/questions/15745385/memory-get-peak-usage-with-real-usage
745
		// Also we use the safer peak memory usage
746
		return (float)memory_get_peak_usage(false);
747
	}
748
749
	/**
750
	 * Determines the memory limit (in bytes) for this application
751
	 * Limits to the smaller of memory_limit configured via php.ini or silverstripe config
752
	 *
753
	 * @return float Memory limit in bytes
754
	 */
755
	protected function getMemoryLimit() {
756
		// Limit to smaller of explicit limit or php memory limit
757
		$limit = $this->parseMemory(Config::inst()->get(__CLASS__, 'memory_limit'));
758
		if($limit) {
759
			return $limit;
760
		}
761
762
		// Fallback to php memory limit
763
		$phpLimit = $this->getPHPMemoryLimit();
764
		if($phpLimit) {
765
			return $phpLimit;
766
		}
767
	}
768
769
	/**
770
	 * Calculate the current memory limit of the server
771
	 *
772
	 * @return float
773
	 */
774
	protected function getPHPMemoryLimit() {
775
		return $this->parseMemory(trim(ini_get("memory_limit")));
776
	}
777
778
	/**
779
	 * Convert memory limit string to bytes.
780
	 * Based on implementation in install.php5
781
	 *
782
	 * @param string $memString
783
	 * @return float
784
	 */
785
	protected function parseMemory($memString) {
786
		switch(strtolower(substr($memString, -1))) {
787
			case "b":
788
				return round(substr($memString, 0, -1));
789
			case "k":
790
				return round(substr($memString, 0, -1) * 1024);
791
			case "m":
792
				return round(substr($memString, 0, -1) * 1024 * 1024);
793
			case "g":
794
				return round(substr($memString, 0, -1) * 1024 * 1024 * 1024);
795
			default:
796
				return round($memString);
797
		}
798
	}
799
800
	protected function humanReadable($size) {
801
		$filesizename = array(" Bytes", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB");
802
		return $size ? round($size/pow(1024, ($i = floor(log($size, 1024)))), 2) . $filesizename[$i] : '0 Bytes';
803
	}
804
805
806
	/**
807
	 * Gets a list of all the current jobs (or jobs that have recently finished)
808
	 *
809
	 * @param string $type
810
	 *			if we're after a particular job list
811
	 * @param int $includeUpUntil
812
	 *			The number of seconds to include jobs that have just finished, allowing a job list to be built that
813
	 *			includes recently finished jobs
814
	 */
815
	public function getJobList($type = null, $includeUpUntil = 0) {
816
		return DataObject::get('QueuedJobDescriptor', $this->getJobListFilter($type, $includeUpUntil));
817
	}
818
819
	/**
820
	 * Return the SQL filter used to get the job list - this is used by the UI for displaying the job list...
821
	 *
822
	 * @param string $type
823
	 *			if we're after a particular job list
824
	 * @param int $includeUpUntil
825
	 *			The number of seconds to include jobs that have just finished, allowing a job list to be built that
826
	 *			includes recently finished jobs
827
	 * @return string
828
	 */
829
	public function getJobListFilter($type = null, $includeUpUntil = 0) {
830
		$filter = array('JobStatus <>' => QueuedJob::STATUS_COMPLETE);
831
		if ($includeUpUntil) {
832
			$filter['JobFinished > '] = date('Y-m-d H:i:s', time() - $includeUpUntil);
833
		}
834
835
		$filter = singleton('QJUtils')->dbQuote($filter, ' OR ');
836
837
		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...
838
			$filter = singleton('QJUtils')->dbQuote(array('JobType =' => (string) $type)) . ' AND ('.$filter.')';
839
		}
840
841
		return $filter;
842
	}
843
844
	/**
845
	 * Process the job queue with the current queue runner
846
	 *
847
	 * @param string $queue
848
	 */
849
	public function runQueue($queue) {
850
		$this->checkJobHealth($queue);
851
		$this->checkdefaultJobs($queue);
852
		$this->queueRunner->runQueue($queue);
853
	}
854
855
	/**
856
	 * Process all jobs from a given queue
857
	 *
858
	 * @param string $name The job queue to completely process
859
	 */
860
	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...
861
		// Start timer to measure lifetime
862
		$this->markStarted();
863
864
		// Begin main loop
865
		do {
866
			if (class_exists('Subsite')) {
867
				// clear subsite back to default to prevent any subsite changes from leaking to
868
				// subsequent actions
869
				Subsite::changeSubsite(0);
870
			}
871
			if (Controller::has_curr()) {
872
				Session::clear('loggedInAs');
873
			} else {
874
				unset($_SESSION['loggedInAs']);
875
			}
876
877
			if (class_exists('SecurityContext')) {
878
				singleton('SecurityContext')->setMember(null);
879
			}
880
881
			$job = $this->getNextPendingJob($name);
882
			if ($job) {
883
				$success = $this->runJob($job->ID);
884
				if (!$success) {
885
					// make sure job is null so it doesn't continue the current
886
					// processing loop. Next queue executor can pick up where
887
					// things left off
888
					$job = null;
889
				}
890
			}
891
		} while($job);
892
	}
893
894
	/**
895
	 * When PHP shuts down, we want to process all of the immediate queue items
896
	 *
897
	 * We use the 'getNextPendingJob' method, instead of just iterating the queue, to ensure
898
	 * we ignore paused or stalled jobs.
899
	 */
900
	public function onShutdown() {
901
		$this->processJobQueue(QueuedJob::IMMEDIATE);
902
	}
903
}
904
905
/**
906
 * Class used to handle errors for a single job
907
 */
908
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...
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
909
	public function __construct() {
910
		set_error_handler(array($this, 'handleError'));
911
	}
912
913
	public function clear() {
914
		restore_error_handler();
915
	}
916
917
	/**
918
	 * For logging and catching exceptions thrown during AbstractQueuedJob::process()
919
	 * and similar.
920
	 */ 
921
	public function handleException($exception) {
922
		$errno = E_USER_ERROR;
923
		$type = get_class($exception);
924
		$message = "Uncaught " . $type . ": " . $exception->getMessage();
925
		$file = $exception->getFile();
926
		$line = $exception->getLine();
927
		$context = $exception->getTrace();
928
929
		// NOTE: This will call SS_Log::log()
930
		Debug::fatalHandler($errno, $message, $file, $line, $context);
0 ignored issues
show
Documentation introduced by
$errno is of type integer, but the function expects a object<unknown_type>.

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...
Documentation introduced by
$message is of type string, but the function expects a object<unknown_type>.

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...
931
	}
932
933
	/**
934
	 * Works like the core Silverstripe error handler without exiting
935
	 * on fatal messages.
936
	 */ 
937
	public function handleError($errno, $errstr, $errfile, $errline) {
938
		if (error_reporting()) {
939
			// Don't throw E_DEPRECATED in PHP 5.3+
940
			/*if (defined('E_DEPRECATED')) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
49% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
941
				if ($errno == E_DEPRECATED || $errno == E_USER_DEPRECATED) {
942
					return;
943
				}
944
			}*/
945
946
			switch ($errno) {
947
				case E_NOTICE:
948
				case E_USER_NOTICE:
949
				case E_DEPRECATED:
950
				case E_USER_DEPRECATED:
951
				case E_STRICT:
952
					Debug::noticeHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
953
				break;
954
955
				case E_WARNING:
956
				case E_CORE_WARNING:
957
				case E_USER_WARNING:
958
				case E_RECOVERABLE_ERROR:
959
					Debug::warningHandler($errno, $errstr, $errfile, $errline, debug_backtrace());
0 ignored issues
show
Documentation introduced by
debug_backtrace() is of type array, but the function expects a object<unknown_type>.

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...
960
				break;
961
962
				default:
963
					throw new ErrorException($errstr, $errno, $errno, $errfile, $errline);
964
					// Old exception throw
965
					//throw new Exception($errstr . " in $errfile at line $errline", $errno);
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
966
				break;
0 ignored issues
show
Unused Code introduced by
// Old exception throw /...rline", $errno); 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...
967
			}
968
		}
969
	}
970
}
971