This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php declare(strict_types=1); |
||
2 | |||
3 | namespace Terah\Assert; |
||
4 | |||
5 | use Closure; |
||
6 | |||
7 | class Tester |
||
8 | { |
||
9 | const DEFAULT_SUITE = 'default'; |
||
10 | |||
11 | /** @var string */ |
||
12 | protected static $currentSuite = self::DEFAULT_SUITE; |
||
13 | |||
14 | /** @var Suite[] */ |
||
15 | protected static $suites = []; |
||
16 | |||
17 | /** @var Logger $logger */ |
||
18 | public static $logger = null; |
||
19 | |||
20 | /** |
||
21 | * @return bool |
||
22 | */ |
||
23 | public static function init() : bool |
||
24 | { |
||
25 | return true; |
||
26 | } |
||
27 | |||
28 | /** |
||
29 | * @param string $name |
||
30 | * @return Suite |
||
31 | */ |
||
32 | public static function suite(string $name='') : Suite |
||
33 | { |
||
34 | $name = $name ?: static::$currentSuite; |
||
35 | static::$suites[$name] = new Suite(); |
||
36 | |||
37 | return static::$suites[$name]; |
||
38 | } |
||
39 | |||
40 | /** |
||
41 | * @param string $testName |
||
42 | * @param Closure $test |
||
43 | * @param string $suiteName |
||
44 | * @param string $successMessage |
||
45 | * @param int|null $exceptionCode |
||
46 | * @param string $exceptionClass |
||
47 | * @param string $exceptionMsg |
||
48 | * @return Suite |
||
49 | * @throws AssertionFailedException |
||
50 | */ |
||
51 | public static function test(string $testName, Closure $test, string $suiteName='', string $successMessage='', int $exceptionCode=0, string $exceptionClass='', string $exceptionMsg='') : Suite |
||
52 | { |
||
53 | Assert::that($successMessage)->notEmpty(); |
||
54 | Assert::that($test)->isCallable(); |
||
55 | Assert::that($suiteName)->notEmpty(); |
||
56 | |||
57 | return static::suite($suiteName)->test($testName, $test, $successMessage, $exceptionCode, $exceptionClass, $exceptionMsg); |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | * @param string $suiteName |
||
62 | * @param string $testName |
||
63 | * @return array |
||
64 | */ |
||
65 | public static function run(string $suiteName='', string $testName='') : array |
||
66 | { |
||
67 | $totalFailed = 0; |
||
68 | $totalTests = 0; |
||
69 | $suites = static::$suites; |
||
70 | if ( ! empty($suiteName) ) |
||
71 | { |
||
72 | Assert::that($suites)->keyExists($suiteName, "The test suite ({$suiteName}) has not been loaded"); |
||
73 | $suites = [$suites[$suiteName]]; |
||
74 | } |
||
75 | foreach ( $suites as $suite ) |
||
76 | { |
||
77 | $totalFailed += $suite->run($testName); |
||
78 | $totalTests += $suite->totalTestsCount(); |
||
79 | } |
||
80 | |||
81 | return compact('totalFailed', 'totalTests'); |
||
82 | } |
||
83 | |||
84 | /** |
||
85 | * @return Logger |
||
86 | */ |
||
87 | public static function getLogger() : Logger |
||
88 | { |
||
89 | if ( ! static::$logger ) |
||
90 | { |
||
91 | static::$logger = new Logger(); |
||
92 | } |
||
93 | |||
94 | return static::$logger; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * @param string $suiteName |
||
99 | * @return Suite |
||
100 | */ |
||
101 | protected static function getSuite(string $suiteName='') : Suite |
||
102 | { |
||
103 | $suiteName = $suiteName ?: static::$currentSuite; |
||
104 | if ( ! array_key_exists($suiteName, static::$suites) ) |
||
105 | { |
||
106 | return static::suite($suiteName); |
||
107 | } |
||
108 | |||
109 | return static::$suites[$suiteName]; |
||
110 | } |
||
111 | |||
112 | |||
113 | /** |
||
114 | * @param string $inputFile |
||
115 | * @param string $outputPath |
||
116 | * @return bool |
||
117 | */ |
||
118 | public static function generateTest(string $inputFile, string $outputPath) : bool |
||
119 | { |
||
120 | $declaredClasses = get_declared_classes(); |
||
121 | require $inputFile; //one or more classes in file, contains class class1, class2, etc... |
||
122 | |||
123 | $className = array_values(array_diff_key(get_declared_classes(), $declaredClasses)); |
||
124 | |||
125 | $reflectionClass = new \ReflectionClass($className[0]); |
||
126 | $publicMethods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); |
||
127 | $fullClassName = $reflectionClass->getName(); |
||
128 | $className = $reflectionClass->getShortName(); |
||
129 | $namespace = $reflectionClass->getNamespaceName(); |
||
130 | $constructorParams = ''; |
||
131 | foreach ( $publicMethods as $method ) |
||
132 | { |
||
133 | if ( $method->isConstructor() ) |
||
134 | { |
||
135 | $constructorParams = static::getMethodParams($method); |
||
136 | } |
||
137 | } |
||
138 | $objectInit = "new {$fullClassName}({$constructorParams})"; |
||
139 | $output = []; |
||
140 | $output[] = <<<PHP |
||
141 | <?php declare(strict_types=1); |
||
142 | |||
143 | namespace {$namespace}\Test; |
||
144 | |||
145 | use Terah\Assert\Assert; |
||
146 | use Terah\Assert\Tester; |
||
147 | use Terah\Assert\Suite; |
||
148 | |||
149 | Tester::suite('AssertSuite') |
||
150 | |||
151 | ->fixture('testSubject', {$objectInit}) |
||
152 | PHP; |
||
153 | |||
154 | foreach ( $publicMethods as $method ) |
||
155 | { |
||
156 | $methodName = $method->getName(); |
||
157 | $methodParams = static::getMethodParams($method); |
||
158 | $testName = 'test' . ucfirst($methodName); |
||
159 | $successArgs = static::getMethodArgs($method); |
||
160 | $failArgs = static::getMethodArgs($method, ' '); |
||
161 | $returnVal = static::getReturnVal($method); |
||
162 | $methodSignature = "\$suite->getFixture('testSubject')->{$methodName}({$methodParams})"; |
||
163 | |||
164 | if ( $method->isStatic() ) |
||
165 | { |
||
166 | $methodSignature = "{$className}::{$methodName}({$methodParams})"; |
||
167 | } |
||
168 | |||
169 | $output[] = <<<PHP |
||
170 | |||
171 | ->test('{$testName}Success', function(Suite \$suite) { |
||
172 | |||
173 | {$successArgs} |
||
174 | \$actual = {$methodSignature}; |
||
175 | \$expected = {$returnVal}; |
||
176 | |||
177 | Assert::that(\$actual))->eq(\$expected, 'The method ({$methodName}) did not produce the correct output'); |
||
178 | }) |
||
179 | |||
180 | ->test('{$testName}Failure', function(Suite \$suite) { |
||
181 | |||
182 | {$failArgs} |
||
183 | \$actual = {$methodSignature}; |
||
184 | \$expected = {$returnVal}; |
||
185 | |||
186 | Assert::that(\$actual))->eq(\$expected, 'The method ({$methodName}) did not produce the correct output'); |
||
187 | |||
188 | }, '', Assert::INVALID_INTEGER, AssertionFailedException::class) |
||
189 | PHP; |
||
190 | |||
191 | } |
||
192 | |||
193 | $output[] = " ;"; |
||
194 | |||
195 | return static::createDirectoriesAndSaveFile($outputPath, implode("\n", $output)); |
||
196 | } |
||
197 | |||
198 | |||
199 | /** |
||
200 | * @param string $filePath |
||
201 | * @param string $data |
||
202 | * @param int $flags |
||
203 | * @param int $dirMode |
||
204 | * @return bool |
||
205 | */ |
||
206 | protected static function createDirectoriesAndSaveFile(string $filePath, $data, $flags=0, $dirMode=0755) : bool |
||
207 | { |
||
208 | static::createParentDirectories($filePath, $dirMode); |
||
209 | Assert::that(file_put_contents($filePath, $data, $flags))->notFalse("Failed to put contents in file ({$filePath})"); |
||
210 | |||
211 | return true; |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * @param string $filePath |
||
216 | * @param int $mode |
||
217 | * @return bool |
||
218 | */ |
||
219 | protected static function createParentDirectories(string $filePath, $mode=0755) : bool |
||
220 | { |
||
221 | $directoryPath = preg_match('/.*\//', $filePath); |
||
222 | Assert::that($filePath) |
||
223 | ->notEmpty("Failed to identify path ({$directoryPath}) to create") |
||
224 | ->notEq(DIRECTORY_SEPARATOR, "Failed to identify path ({$directoryPath}) to create"); |
||
225 | if ( file_exists($directoryPath) ) |
||
226 | { |
||
227 | Assert::that(is_dir($directoryPath))->notFalse("Failed to create parent directories.. files exists and is not a directory({$directoryPath})"); |
||
228 | |||
229 | return true; |
||
230 | } |
||
231 | Assert::that(mkdir($directoryPath, $mode, true))->notFalse("Failed to create parent directories ({$directoryPath})"); |
||
232 | Assert::that($directoryPath)->directory(); |
||
233 | |||
234 | return true; |
||
235 | } |
||
236 | |||
237 | /** |
||
238 | * @param \ReflectionMethod $method |
||
239 | * @return string |
||
240 | */ |
||
241 | protected static function getMethodParams(\ReflectionMethod $method) : string |
||
242 | { |
||
243 | $output = []; |
||
244 | foreach ( $method->getParameters() as $param ) |
||
245 | { |
||
246 | $output[] = '$' . $param->getName(); |
||
247 | } |
||
248 | |||
249 | return implode(', ', $output); |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * @param \ReflectionMethod $method |
||
254 | * @param string $extraPadding |
||
255 | * @return string |
||
256 | */ |
||
257 | protected static function getMethodArgs(\ReflectionMethod $method, string $extraPadding='') : string |
||
258 | { |
||
259 | $output = []; |
||
260 | $params = $method->getParameters(); |
||
261 | foreach ( $params as $param ) |
||
262 | { |
||
263 | $type = $param->hasType() ? $param->getType()->_toString() : ''; |
||
264 | $paramDef = str_pad('$' . $param->getName(), 32, ' ') . '= '; |
||
265 | $paramDef .= static::getDefaultValue($type); |
||
266 | $output[] = $paramDef . ';'; |
||
267 | } |
||
268 | |||
269 | return implode("\n {$extraPadding}", $output); |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * @param \ReflectionMethod $method |
||
274 | * @return string |
||
275 | */ |
||
276 | protected static function getReturnVal(\ReflectionMethod $method) : string |
||
277 | { |
||
278 | $returnType = $method->hasReturnType() ? $method->getReturnType()->_toString() : ''; |
||
279 | |||
280 | return static::getDefaultValue($returnType); |
||
281 | } |
||
282 | |||
283 | /** |
||
284 | * @param string $type |
||
285 | * @param string $default |
||
286 | * @return string |
||
287 | */ |
||
288 | protected static function getDefaultValue(string $type='', string $default='null') : string |
||
289 | { |
||
290 | $typeMap = [ |
||
291 | 'int' => "0", |
||
292 | 'float' => "0.0", |
||
293 | 'string' => "''", |
||
294 | 'bool' => "false", |
||
295 | 'stdClass' => "new stdClass", |
||
296 | 'array' => "[]", |
||
297 | ]; |
||
298 | |||
299 | return $typeMap[$type] ?? $default; |
||
300 | } |
||
301 | } |
||
302 | |||
303 | |||
304 | class Suite |
||
0 ignored issues
–
show
|
|||
305 | { |
||
306 | /** @var Closure[] */ |
||
307 | protected $setUps = []; |
||
308 | |||
309 | /** @var Closure[] */ |
||
310 | protected $tearDowns = []; |
||
311 | |||
312 | /** @var Test[] */ |
||
313 | protected $tests = []; |
||
314 | |||
315 | /** @var mixed[] */ |
||
316 | protected $fixtures = []; |
||
317 | |||
318 | /** @var Logger */ |
||
319 | protected $logger = null; |
||
320 | |||
321 | /** @var int **/ |
||
322 | protected $failedCount = 0; |
||
323 | |||
324 | /** |
||
325 | * @param string $filter |
||
326 | * @return int |
||
327 | */ |
||
328 | public function run(string $filter='') : int |
||
329 | { |
||
330 | foreach ( $this->tests as $test => $testCase ) |
||
331 | { |
||
332 | $testName = $testCase->getTestName(); |
||
333 | if ( $filter && $testName !== $filter ) |
||
334 | { |
||
335 | continue; |
||
336 | } |
||
337 | try |
||
338 | { |
||
339 | $this->getLogger()->info("[{$testName}] - Starting..."); |
||
340 | foreach ( $this->setUps as $idx => $closure ) |
||
341 | { |
||
342 | $closure->__invoke($this); |
||
343 | } |
||
344 | $testCase->runTest($this); |
||
345 | foreach ( $this->tearDowns as $idx => $closure ) |
||
346 | { |
||
347 | $closure->__invoke($this); |
||
348 | } |
||
349 | $this->getLogger()->info("[{$testName}] - " . $testCase->getSuccessMessage()); |
||
350 | } |
||
351 | catch ( \Exception $e ) |
||
352 | { |
||
353 | $expectedCode = $testCase->getExceptionCode(); |
||
354 | $expectedClass = $testCase->getExceptionType(); |
||
355 | $expectedMsg = $testCase->getExceptionMsg(); |
||
356 | $code = $e->getCode(); |
||
357 | $message = $e->getMessage(); |
||
358 | $exception = get_class($e); |
||
359 | if ( ! $expectedClass && ! $expectedCode ) |
||
360 | { |
||
361 | $this->getLogger()->error($e->getMessage(), [compact('testName'), $e]); |
||
362 | $this->failedCount++; |
||
363 | |||
364 | continue; |
||
365 | } |
||
366 | View Code Duplication | if ( $expectedCode && $expectedCode !== $code ) |
|
367 | { |
||
368 | $this->getLogger()->error("Exception code({$code}) was expected to be ({$expectedCode})", [compact('testName'), $e]); |
||
369 | $this->failedCount++; |
||
370 | |||
371 | continue; |
||
372 | } |
||
373 | View Code Duplication | if ( $expectedMsg && $expectedMsg !== $message ) |
|
374 | { |
||
375 | $this->getLogger()->error("Exception message({$message}) was expected to be ({$expectedMsg})", [compact('testName'), $e]); |
||
376 | $this->failedCount++; |
||
377 | |||
378 | continue; |
||
379 | } |
||
380 | View Code Duplication | if ( $expectedClass && $expectedClass !== $exception ) |
|
381 | { |
||
382 | $this->getLogger()->error("Exception class({$exception}) was expected to be ({$expectedClass})", [compact('testName'), $e]); |
||
383 | $this->failedCount++; |
||
384 | |||
385 | continue; |
||
386 | } |
||
387 | $this->getLogger()->info("[{$test}] - " . $testCase->getSuccessMessage()); |
||
388 | } |
||
389 | } |
||
390 | |||
391 | return $this->failedTestsCount(); |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * @return int |
||
396 | */ |
||
397 | public function totalTestsCount() : int |
||
398 | { |
||
399 | return count($this->tests); |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * @return int |
||
404 | */ |
||
405 | public function failedTestsCount() : int |
||
406 | { |
||
407 | return $this->failedCount; |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * @param Closure $callback |
||
412 | * @return Suite |
||
413 | */ |
||
414 | public function setUp(Closure $callback) : Suite |
||
415 | { |
||
416 | $this->setUps[] = $callback; |
||
417 | |||
418 | return $this; |
||
419 | } |
||
420 | |||
421 | /** |
||
422 | * @param Closure $callback |
||
423 | * @return Suite |
||
424 | */ |
||
425 | public function tearDown(Closure $callback) : Suite |
||
426 | { |
||
427 | $this->tearDowns[] = $callback; |
||
428 | |||
429 | return $this; |
||
430 | } |
||
431 | |||
432 | /** |
||
433 | * @param string $testName |
||
434 | * @param Closure $test |
||
435 | * @param string $successMessage |
||
436 | * @param int|null $exceptionCode |
||
437 | * @param string $exceptionClass |
||
438 | * @return Suite |
||
439 | * @throws AssertionFailedException |
||
440 | */ |
||
441 | public function test(string $testName, Closure $test, string $successMessage='', int $exceptionCode=0, string $exceptionClass='', string $exceptionMsg='') : Suite |
||
442 | { |
||
443 | $this->tests[] = new Test($testName, $test, $successMessage, $exceptionCode, $exceptionClass, $exceptionMsg); |
||
444 | |||
445 | return $this; |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * @param string $fixtureName |
||
450 | * @param $value |
||
451 | * @return Suite |
||
452 | */ |
||
453 | public function fixture(string $fixtureName, $value) : Suite |
||
454 | { |
||
455 | $this->fixtures[$fixtureName] = $value; |
||
456 | |||
457 | return $this; |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * @param string $fixtureName |
||
462 | * @return mixed |
||
463 | * @throws AssertionFailedException |
||
464 | */ |
||
465 | public function getFixture(string $fixtureName) |
||
466 | { |
||
467 | Assert::that($this->fixtures)->keyExists($fixtureName, "The fixture ({$fixtureName}) does not exist."); |
||
468 | |||
469 | if ( is_callable($this->fixtures[$fixtureName]) ) |
||
470 | { |
||
471 | $this->fixtures[$fixtureName] = $this->fixtures[$fixtureName]->__invoke($this); |
||
472 | } |
||
473 | |||
474 | return $this->fixtures[$fixtureName]; |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * @param Closure $callback |
||
479 | * @return $this |
||
480 | */ |
||
481 | public function execute(Closure $callback) |
||
482 | { |
||
483 | $callback->__invoke($this); |
||
484 | |||
485 | return $this; |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * @param Logger $logger |
||
490 | * @return $this |
||
491 | */ |
||
492 | public function setLogger(Logger $logger) : Suite |
||
493 | { |
||
494 | $this->logger = $logger; |
||
495 | |||
496 | return $this; |
||
497 | } |
||
498 | |||
499 | /** |
||
500 | * @return Logger |
||
501 | */ |
||
502 | public function getLogger() : Logger |
||
503 | { |
||
504 | if ( ! $this->logger ) |
||
505 | { |
||
506 | $this->logger = new Logger(); |
||
507 | } |
||
508 | |||
509 | return $this->logger; |
||
510 | } |
||
511 | |||
512 | } |
||
513 | |||
514 | class Test |
||
0 ignored issues
–
show
|
|||
515 | { |
||
516 | /** @var string */ |
||
517 | public $testName = ''; |
||
518 | |||
519 | /** @var string */ |
||
520 | public $successMessage = ''; |
||
521 | |||
522 | /** @var Closure */ |
||
523 | public $test = null; |
||
524 | |||
525 | /** @var string */ |
||
526 | public $exceptionType = null; |
||
527 | |||
528 | /** @var int */ |
||
529 | public $exceptionCode = null; |
||
530 | |||
531 | /** @var string */ |
||
532 | public $exceptionMsg = null; |
||
533 | |||
534 | /** |
||
535 | * Test constructor. |
||
536 | * |
||
537 | * @param string $testName |
||
538 | * @param Closure $test |
||
539 | * @param string $successMessage |
||
540 | * @param int $exceptionCode |
||
541 | * @param string $exceptionClass |
||
542 | * @param string $exceptionMsg |
||
543 | * @throws AssertionFailedException |
||
544 | */ |
||
545 | public function __construct(string $testName, Closure $test, string $successMessage='', int $exceptionCode=0, string $exceptionClass='', string $exceptionMsg='') |
||
546 | { |
||
547 | $this->setTestName($testName); |
||
548 | $this->setTest($test); |
||
549 | $this->setSuccessMessage($successMessage); |
||
550 | $this->setExceptionCode($exceptionCode); |
||
551 | $this->setExceptionType($exceptionClass); |
||
552 | $this->setExceptionMsg($exceptionMsg); |
||
553 | } |
||
554 | |||
555 | /** |
||
556 | * @return string |
||
557 | */ |
||
558 | public function getTestName() : string |
||
559 | { |
||
560 | return $this->testName; |
||
561 | } |
||
562 | |||
563 | /** |
||
564 | * @param string $testName |
||
565 | * @return Test |
||
566 | */ |
||
567 | public function setTestName(string $testName) : Test |
||
568 | { |
||
569 | Assert::that($testName)->notEmpty(); |
||
570 | |||
571 | $this->testName = $testName; |
||
572 | |||
573 | return $this; |
||
574 | } |
||
575 | |||
576 | /** |
||
577 | * @return string |
||
578 | */ |
||
579 | public function getSuccessMessage() : string |
||
580 | { |
||
581 | if ( ! $this->successMessage ) |
||
582 | { |
||
583 | return "Successfully run {$this->testName}"; |
||
584 | } |
||
585 | |||
586 | return $this->successMessage; |
||
587 | } |
||
588 | |||
589 | /** |
||
590 | * @param string $successMessage |
||
591 | * @return Test |
||
592 | * @throws AssertionFailedException |
||
593 | */ |
||
594 | public function setSuccessMessage(string $successMessage) : Test |
||
595 | { |
||
596 | $this->successMessage = $successMessage; |
||
597 | |||
598 | return $this; |
||
599 | } |
||
600 | |||
601 | /** |
||
602 | * @return Closure |
||
603 | */ |
||
604 | public function getTest() : Closure |
||
605 | { |
||
606 | return $this->test; |
||
607 | } |
||
608 | |||
609 | /** |
||
610 | * @param Closure $test |
||
611 | * @return Test |
||
612 | */ |
||
613 | public function setTest(Closure $test) : Test |
||
614 | { |
||
615 | $this->test = $test; |
||
616 | |||
617 | return $this; |
||
618 | } |
||
619 | |||
620 | /** |
||
621 | * @return string |
||
622 | */ |
||
623 | public function getExceptionType() : string |
||
624 | { |
||
625 | return $this->exceptionType; |
||
626 | } |
||
627 | |||
628 | /** |
||
629 | * @param string $exceptionType |
||
630 | * @return Test |
||
631 | */ |
||
632 | public function setExceptionType(string $exceptionType) : Test |
||
633 | { |
||
634 | $this->exceptionType = $exceptionType; |
||
635 | |||
636 | return $this; |
||
637 | } |
||
638 | |||
639 | /** |
||
640 | * @return string |
||
641 | */ |
||
642 | public function getExceptionMsg() : string |
||
643 | { |
||
644 | return $this->exceptionMsg; |
||
645 | } |
||
646 | |||
647 | /** |
||
648 | * @param string $exceptionMsg |
||
649 | * @return Test |
||
650 | */ |
||
651 | public function setExceptionMsg(string $exceptionMsg) : Test |
||
652 | { |
||
653 | $this->exceptionMsg = $exceptionMsg; |
||
654 | |||
655 | return $this; |
||
656 | } |
||
657 | |||
658 | /** |
||
659 | * @return int |
||
660 | */ |
||
661 | public function getExceptionCode() : int |
||
662 | { |
||
663 | return $this->exceptionCode; |
||
664 | } |
||
665 | |||
666 | /** |
||
667 | * @param int $exceptionCode |
||
668 | * @return Test |
||
669 | */ |
||
670 | public function setExceptionCode(int $exceptionCode) : Test |
||
671 | { |
||
672 | $this->exceptionCode = $exceptionCode; |
||
673 | |||
674 | return $this; |
||
675 | } |
||
676 | |||
677 | /** |
||
678 | * @param Suite $suite |
||
679 | * @return mixed |
||
680 | */ |
||
681 | public function runTest(Suite $suite) |
||
682 | { |
||
683 | return $this->getTest()->__invoke($suite); |
||
684 | } |
||
685 | } |
||
686 | |||
687 | /** |
||
688 | * Class Logger |
||
689 | * |
||
690 | * @package Terah\Assert |
||
691 | */ |
||
692 | class Logger |
||
0 ignored issues
–
show
|
|||
693 | { |
||
694 | const EMERGENCY = 'emergency'; |
||
695 | const ALERT = 'alert'; |
||
696 | const CRITICAL = 'critical'; |
||
697 | const ERROR = 'error'; |
||
698 | const WARNING = 'warning'; |
||
699 | const NOTICE = 'notice'; |
||
700 | const INFO = 'info'; |
||
701 | const DEBUG = 'debug'; |
||
702 | |||
703 | const BLACK = 'black'; |
||
704 | const DARK_GRAY = 'dark_gray'; |
||
705 | const BLUE = 'blue'; |
||
706 | const LIGHT_BLUE = 'light_blue'; |
||
707 | const GREEN = 'green'; |
||
708 | const LIGHT_GREEN = 'light_green'; |
||
709 | const CYAN = 'cyan'; |
||
710 | const LIGHT_CYAN = 'light_cyan'; |
||
711 | const RED = 'red'; |
||
712 | const LIGHT_RED = 'light_red'; |
||
713 | const PURPLE = 'purple'; |
||
714 | const LIGHT_PURPLE = 'light_purple'; |
||
715 | const BROWN = 'brown'; |
||
716 | const YELLOW = 'yellow'; |
||
717 | const MAGENTA = 'magenta'; |
||
718 | const LIGHT_GRAY = 'light_gray'; |
||
719 | const WHITE = 'white'; |
||
720 | const DEFAULT = 'default'; |
||
721 | const BOLD = 'bold'; |
||
722 | |||
723 | /** @var resource $resource The file handle */ |
||
724 | protected $resource = null; |
||
725 | |||
726 | /** @var string $level */ |
||
727 | protected $level = self::INFO; |
||
728 | |||
729 | /** @var bool $closeLocally */ |
||
730 | protected $closeLocally = false; |
||
731 | |||
732 | /** @var bool */ |
||
733 | protected $addDate = true; |
||
734 | |||
735 | /** @var string */ |
||
736 | protected $separator = ' | '; |
||
737 | |||
738 | /** @var \Closure */ |
||
739 | protected $formatter = null; |
||
740 | |||
741 | /** @var string */ |
||
742 | protected $lastLogEntry = ''; |
||
743 | |||
744 | /** @var bool|null */ |
||
745 | protected $gzipFile = null; |
||
746 | |||
747 | /** @var bool */ |
||
748 | protected $useLocking = false; |
||
749 | |||
750 | /** |
||
751 | * @var array $logLevels List of supported levels |
||
752 | */ |
||
753 | static protected $logLevels = [ |
||
754 | self::EMERGENCY => [1, self::WHITE, self::RED, self::DEFAULT, 'EMERG'], |
||
755 | self::ALERT => [2, self::WHITE, self::YELLOW, self::DEFAULT, 'ALERT'], |
||
756 | self::CRITICAL => [3, self::RED, self::DEFAULT, self::BOLD , 'CRIT'], |
||
757 | self::ERROR => [4, self::RED, self::DEFAULT, self::DEFAULT, 'ERROR'], |
||
758 | self::WARNING => [5, self::YELLOW, self::DEFAULT, self::DEFAULT, 'WARN'], |
||
759 | self::NOTICE => [6, self::CYAN, self::DEFAULT, self::DEFAULT, 'NOTE'], |
||
760 | self::INFO => [7, self::GREEN, self::DEFAULT, self::DEFAULT, 'INFO'], |
||
761 | self::DEBUG => [8, self::LIGHT_GRAY, self::DEFAULT, self::DEFAULT, 'DEBUG'], |
||
762 | ]; |
||
763 | |||
764 | /** |
||
765 | * @var array |
||
766 | */ |
||
767 | static protected $colours = [ |
||
768 | 'fore' => [ |
||
769 | self::BLACK => '0;30', |
||
770 | self::DARK_GRAY => '1;30', |
||
771 | self::BLUE => '0;34', |
||
772 | self::LIGHT_BLUE => '1;34', |
||
773 | self::GREEN => '0;32', |
||
774 | self::LIGHT_GREEN => '1;32', |
||
775 | self::CYAN => '0;36', |
||
776 | self::LIGHT_CYAN => '1;36', |
||
777 | self::RED => '0;31', |
||
778 | self::LIGHT_RED => '1;31', |
||
779 | self::PURPLE => '0;35', |
||
780 | self::LIGHT_PURPLE => '1;35', |
||
781 | self::BROWN => '0;33', |
||
782 | self::YELLOW => '1;33', |
||
783 | self::MAGENTA => '0;35', |
||
784 | self::LIGHT_GRAY => '0;37', |
||
785 | self::WHITE => '1;37', |
||
786 | ], |
||
787 | 'back' => [ |
||
788 | self::DEFAULT => '49', |
||
789 | self::BLACK => '40', |
||
790 | self::RED => '41', |
||
791 | self::GREEN => '42', |
||
792 | self::YELLOW => '43', |
||
793 | self::BLUE => '44', |
||
794 | self::MAGENTA => '45', |
||
795 | self::CYAN => '46', |
||
796 | self::LIGHT_GRAY => '47', |
||
797 | ], |
||
798 | self::BOLD => [], |
||
799 | ]; |
||
800 | |||
801 | /** |
||
802 | * @param mixed $resource |
||
803 | * @param string $level |
||
804 | * @param bool $useLocking |
||
805 | * @param bool $gzipFile |
||
806 | * @param bool $addDate |
||
807 | */ |
||
808 | public function __construct($resource=STDOUT, string $level=self::INFO, bool $useLocking=false, bool $gzipFile=false, bool $addDate=true) |
||
809 | { |
||
810 | $this->resource = $resource; |
||
811 | $this->setLogLevel($level); |
||
812 | $this->useLocking = $useLocking; |
||
813 | $this->gzipFile = $gzipFile; |
||
814 | $this->addDate = $addDate; |
||
815 | } |
||
816 | |||
817 | /** |
||
818 | * System is unusable. |
||
819 | * |
||
820 | * @param string $message |
||
821 | * @param array $context |
||
822 | */ |
||
823 | public function emergency(string $message, array $context=[]) |
||
824 | { |
||
825 | $this->log(self::EMERGENCY, $message, $context); |
||
826 | } |
||
827 | |||
828 | /** |
||
829 | * Action must be taken immediately. |
||
830 | * |
||
831 | * Example: Entire website down, database unavailable, etc. This should |
||
832 | * trigger the SMS alerts and wake you up. |
||
833 | * |
||
834 | * @param string $message |
||
835 | * @param array $context |
||
836 | */ |
||
837 | public function alert(string $message, array $context=[]) |
||
838 | { |
||
839 | $this->log(self::ALERT, $message, $context); |
||
840 | } |
||
841 | |||
842 | /** |
||
843 | * Critical conditions. |
||
844 | * |
||
845 | * Example: Application component unavailable, unexpected exception. |
||
846 | * |
||
847 | * @param string $message |
||
848 | * @param array $context |
||
849 | */ |
||
850 | public function critical(string $message, array $context=[]) |
||
851 | { |
||
852 | $this->log(self::CRITICAL, $message, $context); |
||
853 | } |
||
854 | |||
855 | /** |
||
856 | * Runtime errors that do not require immediate action but should typically |
||
857 | * be logged and monitored. |
||
858 | * |
||
859 | * @param string $message |
||
860 | * @param array $context |
||
861 | */ |
||
862 | public function error(string $message, array $context=[]) |
||
863 | { |
||
864 | $this->log(self::ERROR, $message, $context); |
||
865 | } |
||
866 | |||
867 | /** |
||
868 | * Exceptional occurrences that are not errors. |
||
869 | * |
||
870 | * Example: Use of deprecated APIs, poor use of an API, undesirable things |
||
871 | * that are not necessarily wrong. |
||
872 | * |
||
873 | * @param string $message |
||
874 | * @param array $context |
||
875 | */ |
||
876 | public function warning(string $message, array $context=[]) |
||
877 | { |
||
878 | $this->log(self::WARNING, $message, $context); |
||
879 | } |
||
880 | |||
881 | /** |
||
882 | * Normal but significant events. |
||
883 | * |
||
884 | * @param string $message |
||
885 | * @param array $context |
||
886 | */ |
||
887 | public function notice(string $message, array $context=[]) |
||
888 | { |
||
889 | $this->log(self::NOTICE, $message, $context); |
||
890 | } |
||
891 | |||
892 | /** |
||
893 | * Interesting events. |
||
894 | * |
||
895 | * Example: User logs in, SQL logs. |
||
896 | * |
||
897 | * @param string $message |
||
898 | * @param array $context |
||
899 | */ |
||
900 | public function info(string $message, array $context=[]) |
||
901 | { |
||
902 | $this->log(self::INFO, $message, $context); |
||
903 | } |
||
904 | |||
905 | /** |
||
906 | * Detailed debug information. |
||
907 | * |
||
908 | * @param string $message |
||
909 | * @param array $context |
||
910 | */ |
||
911 | public function debug(string $message, array $context=[]) |
||
912 | { |
||
913 | $this->log(self::DEBUG, $message, $context); |
||
914 | } |
||
915 | |||
916 | /** |
||
917 | * @param $resource |
||
918 | * @return Logger |
||
919 | */ |
||
920 | public function setLogFile($resource) : Logger |
||
921 | { |
||
922 | $this->resource = $resource; |
||
923 | |||
924 | return $this; |
||
925 | } |
||
926 | |||
927 | /** |
||
928 | * @param string $string |
||
929 | * @param string $foregroundColor |
||
930 | * @param string $backgroundColor |
||
931 | * @param bool $bold |
||
932 | * @return string |
||
933 | */ |
||
934 | public static function addColour(string $string, string $foregroundColor='', string $backgroundColor='', bool $bold=false) : string |
||
935 | { |
||
936 | // todo: support bold |
||
937 | unset($bold); |
||
938 | $coloredString = ''; |
||
939 | // Check if given foreground color found |
||
940 | View Code Duplication | if ( isset(static::$colours['fore'][$foregroundColor]) ) |
|
941 | { |
||
942 | $coloredString .= "\033[" . static::$colours['fore'][$foregroundColor] . "m"; |
||
943 | } |
||
944 | // Check if given background color found |
||
945 | View Code Duplication | if ( isset(static::$colours['back'][$backgroundColor]) ) |
|
946 | { |
||
947 | $coloredString .= "\033[" . static::$colours['back'][$backgroundColor] . "m"; |
||
948 | } |
||
949 | // Add string and end coloring |
||
950 | $coloredString .= $string . "\033[0m"; |
||
951 | |||
952 | return $coloredString; |
||
953 | } |
||
954 | |||
955 | /** |
||
956 | * @param string $string |
||
957 | * @param string $foregroundColor |
||
958 | * @param string $backgroundColor |
||
959 | * @param bool $bold |
||
960 | * @return string |
||
961 | */ |
||
962 | public function colourize(string $string, string $foregroundColor='', string $backgroundColor='', bool $bold=false) : string |
||
963 | { |
||
964 | return static::addColour($string, $foregroundColor, $backgroundColor, $bold); |
||
965 | } |
||
966 | |||
967 | /** |
||
968 | * @param string $level Ignore logging attempts at a level less the $level |
||
969 | * @return Logger |
||
970 | */ |
||
971 | public function setLogLevel(string $level) : Logger |
||
972 | { |
||
973 | if ( ! isset(static::$logLevels[$level]) ) |
||
974 | { |
||
975 | throw new \InvalidArgumentException("Log level is invalid"); |
||
976 | } |
||
977 | $this->level = static::$logLevels[$level][0]; |
||
978 | |||
979 | return $this; |
||
980 | } |
||
981 | |||
982 | /** |
||
983 | * @return Logger |
||
984 | */ |
||
985 | public function lock() : Logger |
||
986 | { |
||
987 | $this->useLocking = true; |
||
988 | |||
989 | return $this; |
||
990 | } |
||
991 | |||
992 | /** |
||
993 | * @return Logger |
||
994 | */ |
||
995 | public function gzipped() : Logger |
||
996 | { |
||
997 | $this->gzipFile = true; |
||
998 | |||
999 | return $this; |
||
1000 | } |
||
1001 | |||
1002 | /** |
||
1003 | * @param callable $fnFormatter |
||
1004 | * |
||
1005 | * @return Logger |
||
1006 | */ |
||
1007 | public function formatter(callable $fnFormatter) : Logger |
||
1008 | { |
||
1009 | $this->formatter = $fnFormatter; |
||
1010 | |||
1011 | return $this; |
||
1012 | } |
||
1013 | |||
1014 | /** |
||
1015 | * Log messages to resource |
||
1016 | * |
||
1017 | * @param mixed $level The level of the log message |
||
1018 | * @param string $message If an object is passed it must implement __toString() |
||
1019 | * @param array $context Placeholders to be substituted in the message |
||
1020 | */ |
||
1021 | public function log($level, $message, array $context=[]) |
||
1022 | { |
||
1023 | $level = isset(static::$logLevels[$level]) ? $level : self::INFO; |
||
1024 | list($logLevel, $fore, $back, $style) = static::$logLevels[$level]; |
||
1025 | unset($style); |
||
1026 | if ( $logLevel > $this->level ) |
||
1027 | { |
||
1028 | return ; |
||
1029 | } |
||
1030 | if ( is_callable($this->formatter) ) |
||
1031 | { |
||
1032 | $message = $this->formatter->__invoke(static::$logLevels[$level][4], $message, $context); |
||
1033 | } |
||
1034 | else |
||
1035 | { |
||
1036 | $message = $this->formatMessage($level, $message, $context); |
||
1037 | } |
||
1038 | $this->lastLogEntry = $message; |
||
1039 | $this->write($this->colourize($message, $fore, $back) . PHP_EOL); |
||
1040 | } |
||
1041 | |||
1042 | /** |
||
1043 | * @param string $style |
||
1044 | * @param string $message |
||
1045 | * @return string |
||
1046 | */ |
||
1047 | public static function style(string $style, string $message) : string |
||
1048 | { |
||
1049 | $style = isset(static::$logLevels[$style]) ? $style : self::INFO; |
||
1050 | list($logLevel, $fore, $back, $style) = static::$logLevels[$style]; |
||
1051 | unset($logLevel, $style); |
||
1052 | |||
1053 | return static::addColour($message, $fore, $back); |
||
1054 | } |
||
1055 | |||
1056 | /** |
||
1057 | * @param string $level |
||
1058 | * @param string $message |
||
1059 | * @param array $context |
||
1060 | * @return string |
||
1061 | */ |
||
1062 | protected function formatMessage(string $level, string $message, array $context=[]) : string |
||
1063 | { |
||
1064 | # Handle objects implementing __toString |
||
1065 | $message = (string) $message; |
||
1066 | $message .= empty($context) ? '' : PHP_EOL . json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); |
||
1067 | $data = $this->addDate ? ['date' => date('Y-m-d H:i:s')] : []; |
||
1068 | $data['level'] = strtoupper(str_pad(static::$logLevels[$level][4], 5, ' ', STR_PAD_RIGHT)); |
||
1069 | $data['message'] = $message; |
||
1070 | |||
1071 | return implode($this->separator, $data); |
||
1072 | } |
||
1073 | |||
1074 | /** |
||
1075 | * Write the content to the stream |
||
1076 | * |
||
1077 | * @param string $content |
||
1078 | */ |
||
1079 | public function write(string $content) |
||
1080 | { |
||
1081 | $resource = $this->getResource(); |
||
1082 | if ( $this->useLocking ) |
||
1083 | { |
||
1084 | flock($resource, LOCK_EX); |
||
1085 | } |
||
1086 | gzwrite($resource, $content); |
||
1087 | if ( $this->useLocking ) |
||
1088 | { |
||
1089 | flock($resource, LOCK_UN); |
||
1090 | } |
||
1091 | } |
||
1092 | |||
1093 | /** |
||
1094 | * @return mixed|resource |
||
1095 | * @throws \Exception |
||
1096 | */ |
||
1097 | protected function getResource() |
||
1098 | { |
||
1099 | if ( is_resource($this->resource) ) |
||
1100 | { |
||
1101 | return $this->resource; |
||
1102 | } |
||
1103 | $fileName = $this->resource; |
||
1104 | $this->closeLocally = true; |
||
1105 | $this->resource = $this->openResource(); |
||
1106 | if ( ! is_resource($this->resource) ) |
||
1107 | { |
||
1108 | throw new \Exception("The resource ({$fileName}) could not be opened"); |
||
1109 | } |
||
1110 | |||
1111 | return $this->resource; |
||
1112 | } |
||
1113 | |||
1114 | /** |
||
1115 | * @return string |
||
1116 | */ |
||
1117 | public function getLastLogEntry() : string |
||
1118 | { |
||
1119 | return $this->lastLogEntry; |
||
1120 | } |
||
1121 | |||
1122 | /** |
||
1123 | * @return resource |
||
1124 | */ |
||
1125 | protected function openResource() |
||
1126 | { |
||
1127 | if ( $this->gzipFile ) |
||
1128 | { |
||
1129 | return gzopen($this->resource, 'a'); |
||
1130 | } |
||
1131 | |||
1132 | return fopen($this->resource, 'a'); |
||
1133 | } |
||
1134 | |||
1135 | public function __destruct() |
||
1136 | { |
||
1137 | if ($this->closeLocally) |
||
1138 | { |
||
1139 | gzclose($this->getResource()); |
||
1140 | } |
||
1141 | } |
||
1142 | } |
||
1143 |
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.