Completed
Push — master ( 57c37c...bb12e7 )
by Terry
01:43
created

Suite::failedCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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