Tester::getLogger()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
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();
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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();
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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();
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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, ' ') . '= ';
0 ignored issues
show
Bug introduced by
Consider using $param->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
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
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...
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 )
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...
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 )
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...
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 )
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...
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
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...
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
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...
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]) )
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...
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]) )
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...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $fnFormatter of type callable is incompatible with the declared type object<Closure> of property $formatter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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