Completed
Push — master ( dbf744...57c37c )
by Terry
01:51
created

Tester::createParentDirectories()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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