Completed
Push — master ( bb12e7...413e80 )
by Terry
01:43
created

Suite::run()   C

Complexity

Conditions 11
Paths 15

Size

Total Lines 48
Code Lines 28

Duplication

Lines 14
Ratio 29.17 %

Importance

Changes 0
Metric Value
cc 11
eloc 28
nc 15
nop 1
dl 14
loc 48
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * @return array
63
     */
64
    public static function run(string $suiteName='', string $testName='') : array
65
    {
66
        $totalFailed    = 0;
67
        $totalTests     = 0;
68
        $suites         = static::$suites;
69
        if ( ! empty($suiteName) )
70
        {
71
            Assert::that($suites)->keyExists($suiteName, "The test suite ({$suiteName}) has not been loaded");
72
            $suites         = [$suites[$suiteName]];
73
        }
74
        foreach ( $suites as $suite )
75
        {
76
            $totalFailed    += $suite->run($testName);
77
            $totalTests     += $suite->totalTestsCount();
78
        }
79
        
80
        return compact('totalFailed', 'totalTests');
81
    }
82
83
    /**
84
     * @return Logger
85
     */
86
    public static function getLogger() : Logger
87
    {
88
        if ( ! static::$logger )
89
        {
90
            static::$logger = new Logger();
91
        }
92
93
        return static::$logger;
94
    }
95
96
    /**
97
     * @param string $suiteName
98
     * @return Suite
99
     */
100
    protected static function getSuite(string $suiteName='') : Suite
101
    {
102
        $suiteName                  = $suiteName ?: static::$currentSuite;
103
        if ( ! array_key_exists($suiteName, static::$suites) )
104
        {
105
            return static::suite($suiteName);
106
        }
107
108
        return static::$suites[$suiteName];
109
    }
110
111
112
    /**
113
     * @param string $inputFile
114
     * @param string $outputPath
115
     * @return bool
116
     */
117
    public static function generateTest(string $inputFile, string $outputPath) : bool
118
    {
119
        //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...
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
279
        $returnType = $method->hasReturnType() ? $method->getReturnType()->_toString() : '';
280
281
        return static::getDefaultValue($returnType);
282
    }
283
284
    /**
285
     * @param string $type
286
     * @param string $default
287
     * @return string
288
     */
289
    protected static function getDefaultValue(string $type='', string $default='null') : string
290
    {
291
        $typeMap    = [
292
            'int'           => "0",
293
            'float'         => "0.0",
294
            'string'        => "''",
295
            'bool'          => "false",
296
            'stdClass'      => "new stdClass",
297
            'array'         => "[]",
298
        ];
299
300
        return $typeMap[$type] ?? $default;
301
    }
302
}
303
304
305
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...
306
{
307
    /** @var Test[] */
308
    protected $tests        = [];
309
310
    /** @var mixed[] */
311
    protected $fixtures     = [];
312
313
    /** @var Logger */
314
    protected $logger       = null;
315
    
316
    /** @var int **/
317
    protected $failedCount  = 0;
318
    
319
    /**
320
     * @param string $filter
321
     * @return int
322
     */
323
    public function run(string $filter='') : int
324
    {
325
        foreach ( $this->tests as $test => $testCase )
326
        {
327
            $testName   = $testCase->getTestName();
328
            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...
329
            {
330
                continue;
331
            }
332
            try
333
            {
334
                $this->getLogger()->info("[{$testName}] - Starting...");
335
                $testCase->runTest($this);
336
                $this->getLogger()->info("[{$testName}] - " . $testCase->getSuccessMessage());
337
            }
338
            catch ( \Exception $e )
339
            {
340
                $expectedCode       = $testCase->getExceptionCode();
341
                $expectedClass      = $testCase->getExceptionType();
342
                $code               = $e->getCode();
343
                $exception          = get_class($e);
344
                if ( ! $expectedClass &&  ! $expectedCode )
345
                {
346
                    $this->getLogger()->error($e->getMessage(), [compact('testName'), $e]);
347
                    $this->failedCount++;
348
349
                    continue;
350
                }
351 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...
352
                {
353
                    $this->getLogger()->error("Exception code({$code}) was expected to be ({$expectedCode})", [compact('testName'), $e]);
354
                    $this->failedCount++;
355
                    
356
                    continue;
357
                }
358 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...
359
                {
360
                    $this->getLogger()->error("Exception class({$exception}) was expected to be ({$expectedClass})", [compact('testName'), $e]);
361
                    $this->failedCount++;
362
                    
363
                    continue;
364
                }
365
                $this->getLogger()->info("[{$test}] - " . $testCase->getSuccessMessage());
366
            }
367
        }
368
        
369
        return $this->failedTestsCount();
370
    }
371
    
372
    /**
373
     * @return int
374
     */  
375
    public function totalTestsCount() : int
376
    {
377
        return count($this->tests);
378
    }
379
        
380
    /**
381
     * @return int
382
     */  
383
    public function failedTestsCount() : int
384
    {
385
        return $this->failedCount;
386
    }
387
388
    /**
389
     * @param string   $testName
390
     * @param Closure  $test
391
     * @param string   $successMessage
392
     * @param int|null $exceptionCode
393
     * @param string   $exceptionClass
394
     * @return Suite
395
     * @throws AssertionFailedException
396
     */
397
    public function test(string $testName, Closure $test, string $successMessage='', int $exceptionCode=0, string $exceptionClass='') : Suite
398
    {
399
        $this->tests[]  = new Test($testName, $test, $successMessage, $exceptionCode, $exceptionClass);
400
401
        return $this;
402
    }
403
404
    /**
405
     * @param string $fixtureName
406
     * @param        $value
407
     * @return Suite
408
     */
409
    public function fixture(string $fixtureName, $value) : Suite
410
    {
411
        $this->fixtures[$fixtureName]  = $value;
412
413
        return $this;
414
    }
415
416
    /**
417
     * @param string $fixtureName
418
     * @return mixed
419
     * @throws AssertionFailedException
420
     */
421
    public function getFixture(string $fixtureName)
422
    {
423
        Assert::that($this->fixtures)->keyExists($fixtureName, "The fixture ({$fixtureName}) does not exist.");
424
425
        return $this->fixtures[$fixtureName];
426
    }
427
428
429
    /**
430
     * @param Logger $logger
431
     * @return $this
432
     */
433
    public function setLogger(Logger $logger) : Suite
434
    {
435
        $this->logger = $logger;
436
437
        return $this;
438
    }
439
440
    /**
441
     * @return Logger
442
     */
443
    public function getLogger() : Logger
444
    {
445
        if ( ! $this->logger )
446
        {
447
            $this->logger = new Logger();
448
        }
449
450
        return $this->logger;
451
    }
452
453
}
454
455
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...
456
{
457
    /** @var string  */
458
    public $testName        = '';
459
460
    /** @var string  */
461
    public $successMessage  = '';
462
463
    /** @var Closure  */
464
    public $test            = null;
465
466
    /** @var string */
467
    public $exceptionType   = null;
468
469
    /** @var int */
470
    public $exceptionCode   = null;
471
472
    /**
473
     * Test constructor.
474
     *
475
     * @param string   $testName
476
     * @param Closure  $test
477
     * @param string   $successMessage
478
     * @param int|null $exceptionCode
479
     * @param string   $exceptionClass
480
     * @throws AssertionFailedException
481
     */
482
    public function __construct(string $testName, Closure $test, string $successMessage='', int $exceptionCode=0, string $exceptionClass='')
483
    {
484
        $this->setTestName($testName);
485
        $this->setTest($test);
486
        $this->setSuccessMessage($successMessage);
487
        $this->setExceptionCode($exceptionCode);
488
        $this->setExceptionType($exceptionClass);
489
    }
490
491
    /**
492
     * @return string
493
     */
494
    public function getTestName() : string
495
    {
496
        return $this->testName;
497
    }
498
499
    /**
500
     * @param string $testName
501
     * @return Test
502
     */
503
    public function setTestName(string $testName) : Test
504
    {
505
        Assert::that($testName)->notEmpty();
506
507
        $this->testName = $testName;
508
509
        return $this;
510
    }
511
512
    /**
513
     * @return string
514
     */
515
    public function getSuccessMessage() : string
516
    {
517
        if ( ! $this->successMessage )
518
        {
519
            return "Successfully run {$this->testName}";
520
        }
521
522
        return $this->successMessage;
523
    }
524
525
    /**
526
     * @param string $successMessage
527
     * @return Test
528
     * @throws AssertionFailedException
529
     */
530
    public function setSuccessMessage(string $successMessage) : Test
531
    {
532
        $this->successMessage = $successMessage;
533
534
        return $this;
535
    }
536
537
    /**
538
     * @return Closure
539
     */
540
    public function getTest() : Closure
541
    {
542
        return $this->test;
543
    }
544
545
    /**
546
     * @param Closure $test
547
     * @return Test
548
     */
549
    public function setTest(Closure $test) : Test
550
    {
551
        $this->test = $test;
552
553
        return $this;
554
    }
555
556
    /**
557
     * @return string
558
     */
559
    public function getExceptionType() : string
560
    {
561
        return $this->exceptionType;
562
    }
563
564
    /**
565
     * @param string $exceptionType
566
     * @return Test
567
     */
568
    public function setExceptionType(string $exceptionType) : Test
569
    {
570
        $this->exceptionType = $exceptionType;
571
572
        return $this;
573
    }
574
575
    /**
576
     * @return int
577
     */
578
    public function getExceptionCode() : int
579
    {
580
        return $this->exceptionCode;
581
    }
582
583
    /**
584
     * @param int $exceptionCode
585
     * @return Test
586
     */
587
    public function setExceptionCode(int $exceptionCode) : Test
588
    {
589
        $this->exceptionCode = $exceptionCode;
590
591
        return $this;
592
    }
593
594
    /**
595
     * @param Suite $suite
596
     * @return mixed
597
     */
598
    public function runTest(Suite $suite)
599
    {
600
        return $this->getTest()->__invoke($suite);
601
    }
602
}
603
604
/**
605
 * Class Logger
606
 *
607
 * @package Terah\Assert
608
 */
609
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...
610
{
611
    const EMERGENCY     = 'emergency';
612
    const ALERT         = 'alert';
613
    const CRITICAL      = 'critical';
614
    const ERROR         = 'error';
615
    const WARNING       = 'warning';
616
    const NOTICE        = 'notice';
617
    const INFO          = 'info';
618
    const DEBUG         = 'debug';
619
620
    const BLACK         = 'black';
621
    const DARK_GRAY     = 'dark_gray';
622
    const BLUE          = 'blue';
623
    const LIGHT_BLUE    = 'light_blue';
624
    const GREEN         = 'green';
625
    const LIGHT_GREEN   = 'light_green';
626
    const CYAN          = 'cyan';
627
    const LIGHT_CYAN    = 'light_cyan';
628
    const RED           = 'red';
629
    const LIGHT_RED     = 'light_red';
630
    const PURPLE        = 'purple';
631
    const LIGHT_PURPLE  = 'light_purple';
632
    const BROWN         = 'brown';
633
    const YELLOW        = 'yellow';
634
    const MAGENTA       = 'magenta';
635
    const LIGHT_GRAY    = 'light_gray';
636
    const WHITE         = 'white';
637
    const DEFAULT       = 'default';
638
    const BOLD          = 'bold';
639
640
    /**  @var resource $resource The file handle */
641
    protected $resource         = null;
642
643
    /** @var string $level */
644
    protected $level            = self::INFO;
645
646
    /** @var bool $closeLocally */
647
    protected $closeLocally     = false;
648
649
    /** @var bool */
650
    protected $addDate          = true;
651
652
    /** @var string  */
653
    protected $separator        = ' | ';
654
655
    /** @var \Closure */
656
    protected $formatter        = null;
657
658
    /** @var string  */
659
    protected $lastLogEntry     = '';
660
661
    /** @var bool|null  */
662
    protected $gzipFile         = null;
663
664
    /** @var bool  */
665
    protected $useLocking       = false;
666
667
    /**
668
     * @var array $logLevels List of supported levels
669
     */
670
    static protected $logLevels       = [
671
        self::EMERGENCY => [1, self::WHITE,       self::RED,      self::DEFAULT,  'EMERG'],
672
        self::ALERT     => [2, self::WHITE,       self::YELLOW,   self::DEFAULT,  'ALERT'],
673
        self::CRITICAL  => [3, self::RED,         self::DEFAULT,  self::BOLD ,    'CRIT'],
674
        self::ERROR     => [4, self::RED,         self::DEFAULT,  self::DEFAULT,  'ERROR'],
675
        self::WARNING   => [5, self::YELLOW,      self::DEFAULT,  self::DEFAULT,  'WARN'],
676
        self::NOTICE    => [6, self::CYAN,        self::DEFAULT,  self::DEFAULT,  'NOTE'],
677
        self::INFO      => [7, self::GREEN,       self::DEFAULT,  self::DEFAULT,  'INFO'],
678
        self::DEBUG     => [8, self::LIGHT_GRAY,  self::DEFAULT,  self::DEFAULT,  'DEBUG'],
679
    ];
680
681
    /**
682
     * @var array
683
     */
684
    static protected $colours   = [
685
        'fore' => [
686
            self::BLACK         => '0;30',
687
            self::DARK_GRAY     => '1;30',
688
            self::BLUE          => '0;34',
689
            self::LIGHT_BLUE    => '1;34',
690
            self::GREEN         => '0;32',
691
            self::LIGHT_GREEN   => '1;32',
692
            self::CYAN          => '0;36',
693
            self::LIGHT_CYAN    => '1;36',
694
            self::RED           => '0;31',
695
            self::LIGHT_RED     => '1;31',
696
            self::PURPLE        => '0;35',
697
            self::LIGHT_PURPLE  => '1;35',
698
            self::BROWN         => '0;33',
699
            self::YELLOW        => '1;33',
700
            self::MAGENTA       => '0;35',
701
            self::LIGHT_GRAY    => '0;37',
702
            self::WHITE         => '1;37',
703
        ],
704
        'back'  => [
705
            self::DEFAULT       => '49',
706
            self::BLACK         => '40',
707
            self::RED           => '41',
708
            self::GREEN         => '42',
709
            self::YELLOW        => '43',
710
            self::BLUE          => '44',
711
            self::MAGENTA       => '45',
712
            self::CYAN          => '46',
713
            self::LIGHT_GRAY    => '47',
714
        ],
715
        self::BOLD => [],
716
    ];
717
718
    /**
719
     * @param mixed  $resource
720
     * @param string $level
721
     * @param bool   $useLocking
722
     * @param bool   $gzipFile
723
     * @param bool   $addDate
724
     */
725
    public function __construct($resource=STDOUT, string $level=self::INFO, bool $useLocking=false, bool $gzipFile=false, bool $addDate=true)
726
    {
727
        $this->resource     = $resource;
728
        $this->setLogLevel($level);
729
        $this->useLocking   = $useLocking;
730
        $this->gzipFile     = $gzipFile;
731
        $this->addDate      = $addDate;
732
    }
733
734
    /**
735
     * System is unusable.
736
     *
737
     * @param string $message
738
     * @param array $context
739
     */
740
    public function emergency(string $message, array $context=[])
741
    {
742
        $this->log(self::EMERGENCY, $message, $context);
743
    }
744
745
    /**
746
     * Action must be taken immediately.
747
     *
748
     * Example: Entire website down, database unavailable, etc. This should
749
     * trigger the SMS alerts and wake you up.
750
     *
751
     * @param string $message
752
     * @param array $context
753
     */
754
    public function alert(string $message, array $context=[])
755
    {
756
        $this->log(self::ALERT, $message, $context);
757
    }
758
759
    /**
760
     * Critical conditions.
761
     *
762
     * Example: Application component unavailable, unexpected exception.
763
     *
764
     * @param string $message
765
     * @param array $context
766
     */
767
    public function critical(string $message, array $context=[])
768
    {
769
        $this->log(self::CRITICAL, $message, $context);
770
    }
771
772
    /**
773
     * Runtime errors that do not require immediate action but should typically
774
     * be logged and monitored.
775
     *
776
     * @param string $message
777
     * @param array $context
778
     */
779
    public function error(string $message, array $context=[])
780
    {
781
        $this->log(self::ERROR, $message, $context);
782
    }
783
784
    /**
785
     * Exceptional occurrences that are not errors.
786
     *
787
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
788
     * that are not necessarily wrong.
789
     *
790
     * @param string $message
791
     * @param array $context
792
     */
793
    public function warning(string $message, array $context=[])
794
    {
795
        $this->log(self::WARNING, $message, $context);
796
    }
797
798
    /**
799
     * Normal but significant events.
800
     *
801
     * @param string $message
802
     * @param array $context
803
     */
804
    public function notice(string $message, array $context=[])
805
    {
806
        $this->log(self::NOTICE, $message, $context);
807
    }
808
809
    /**
810
     * Interesting events.
811
     *
812
     * Example: User logs in, SQL logs.
813
     *
814
     * @param string $message
815
     * @param array $context
816
     */
817
    public function info(string $message, array $context=[])
818
    {
819
        $this->log(self::INFO, $message, $context);
820
    }
821
822
    /**
823
     * Detailed debug information.
824
     *
825
     * @param string $message
826
     * @param array $context
827
     */
828
    public function debug(string $message, array $context=[])
829
    {
830
        $this->log(self::DEBUG, $message, $context);
831
    }
832
833
    /**
834
     * @param $resource
835
     * @return Logger
836
     */
837
    public function setLogFile($resource) : Logger
838
    {
839
        $this->resource     = $resource;
840
841
        return $this;
842
    }
843
844
    /**
845
     * @param string $string
846
     * @param string $foregroundColor
847
     * @param string $backgroundColor
848
     * @param bool $bold
849
     * @return string
850
     */
851
    public static function addColour(string $string, string $foregroundColor='', string $backgroundColor='', bool $bold=false) : string
852
    {
853
        // todo: support bold
854
        unset($bold);
855
        $coloredString = '';
856
        // Check if given foreground color found
857 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...
858
        {
859
            $coloredString .= "\033[" . static::$colours['fore'][$foregroundColor] . "m";
860
        }
861
        // Check if given background color found
862 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...
863
        {
864
            $coloredString .= "\033[" . static::$colours['back'][$backgroundColor] . "m";
865
        }
866
        // Add string and end coloring
867
        $coloredString .=  $string . "\033[0m";
868
869
        return $coloredString;
870
    }
871
872
    /**
873
     * @param string    $string
874
     * @param string    $foregroundColor
875
     * @param string    $backgroundColor
876
     * @param bool      $bold
877
     * @return string
878
     */
879
    public function colourize(string $string, string $foregroundColor='', string $backgroundColor='', bool $bold=false) : string
880
    {
881
        return static::addColour($string, $foregroundColor, $backgroundColor, $bold);
882
    }
883
884
    /**
885
     * @param string $level Ignore logging attempts at a level less the $level
886
     * @return Logger
887
     */
888
    public function setLogLevel(string $level) : Logger
889
    {
890
        if ( ! isset(static::$logLevels[$level]) )
891
        {
892
            throw new \InvalidArgumentException("Log level is invalid");
893
        }
894
        $this->level = static::$logLevels[$level][0];
895
896
        return $this;
897
    }
898
899
    /**
900
     * @return Logger
901
     */
902
    public function lock() : Logger
903
    {
904
        $this->useLocking = true;
905
906
        return $this;
907
    }
908
909
    /**
910
     * @return Logger
911
     */
912
    public function gzipped() : Logger
913
    {
914
        $this->gzipFile = true;
915
916
        return $this;
917
    }
918
919
    /**
920
     * @param callable $fnFormatter
921
     *
922
     * @return Logger
923
     */
924
    public function formatter(callable $fnFormatter) : Logger
925
    {
926
        $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...
927
928
        return $this;
929
    }
930
931
    /**
932
     * Log messages to resource
933
     *
934
     * @param mixed          $level    The level of the log message
935
     * @param string|object  $message  If an object is passed it must implement __toString()
936
     * @param array          $context  Placeholders to be substituted in the message
937
     */
938
    public function log($level, $message, array $context=[])
939
    {
940
        $level = isset(static::$logLevels[$level]) ? $level : self::INFO;
941
        list($logLevel, $fore, $back, $style) = static::$logLevels[$level];
942
        unset($style);
943
        if ( $logLevel > $this->level )
944
        {
945
            return ;
946
        }
947
        if ( is_callable($this->formatter) )
948
        {
949
            $message = $this->formatter->__invoke(static::$logLevels[$level][4], $message, $context);
950
        }
951
        else
952
        {
953
            $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...
954
        }
955
        $this->lastLogEntry = $message;
956
        $this->write($this->colourize($message, $fore, $back) . PHP_EOL);
957
    }
958
959
    /**
960
     * @param string $style
961
     * @param string $message
962
     * @return string
963
     */
964
    public static function style(string $style, string $message) : string
965
    {
966
        $style = isset(static::$logLevels[$style]) ? $style : self::INFO;
967
        list($logLevel, $fore, $back, $style) = static::$logLevels[$style];
968
        unset($logLevel, $style);
969
970
        return static::addColour($message, $fore, $back);
971
    }
972
973
    /**
974
     * @param string $level
975
     * @param string $message
976
     * @param array  $context
977
     * @return string
978
     */
979
    protected function formatMessage(string $level, string $message, array $context=[]) : string
980
    {
981
        # Handle objects implementing __toString
982
        $message            = (string) $message;
983
        $message            .= empty($context) ? '' : PHP_EOL . json_encode($context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
984
        $data               = $this->addDate ? ['date' => date('Y-m-d H:i:s')] : [];
985
        $data['level']      = strtoupper(str_pad(static::$logLevels[$level][4], 5, ' ', STR_PAD_RIGHT));
986
        $data['message']    = $message;
987
988
        return implode($this->separator, $data);
989
    }
990
991
    /**
992
     * Write the content to the stream
993
     *
994
     * @param  string $content
995
     */
996
    public function write(string $content)
997
    {
998
        $resource = $this->getResource();
999
        if ( $this->useLocking )
1000
        {
1001
            flock($resource, LOCK_EX);
1002
        }
1003
        gzwrite($resource, $content);
1004
        if ( $this->useLocking )
1005
        {
1006
            flock($resource, LOCK_UN);
1007
        }
1008
    }
1009
1010
    /**
1011
     * @return mixed|resource
1012
     * @throws \Exception
1013
     */
1014
    protected function getResource()
1015
    {
1016
        if ( is_resource($this->resource) )
1017
        {
1018
            return $this->resource;
1019
        }
1020
        $fileName               = $this->resource;
1021
        $this->closeLocally     = true;
1022
        $this->resource         = $this->openResource();
1023
        if ( ! is_resource($this->resource) )
1024
        {
1025
            throw new \Exception("The resource ({$fileName}) could not be opened");
1026
        }
1027
1028
        return $this->resource;
1029
    }
1030
1031
    /**
1032
     * @return string
1033
     */
1034
    public function getLastLogEntry() : string
1035
    {
1036
        return $this->lastLogEntry;
1037
    }
1038
1039
    /**
1040
     * @return resource
1041
     */
1042
    protected function openResource()
1043
    {
1044
        if ( $this->gzipFile )
1045
        {
1046
            return gzopen($this->resource, 'a');
1047
        }
1048
1049
        return fopen($this->resource, 'a');
1050
    }
1051
1052
    public function __destruct()
1053
    {
1054
        if ($this->closeLocally)
1055
        {
1056
            gzclose($this->getResource());
1057
        }
1058
    }
1059
}
1060