Completed
Pull Request — master (#157)
by Vítor
03:14
created

App::setOption()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 16
rs 9.4285
cc 3
eloc 8
nc 3
nop 2
1
<?php
2
/**
3
 * This file is part of the PPI Framework.
4
 *
5
 * @copyright  Copyright (c) 2011-2016 Paul Dragoonis <[email protected]>
6
 * @license    http://opensource.org/licenses/mit-license.php MIT
7
 *
8
 * @link       http://www.ppi.io
9
 */
10
11
namespace PPI\Framework;
12
13
use PPI\Framework\Config\ConfigManager;
14
use PPI\Framework\Http\Request as HttpRequest;
15
use PPI\Framework\Http\Response as HttpResponse;
16
use PPI\Framework\Router\ChainRouter;
17
use PPI\Framework\ServiceManager\ServiceManager;
18
use PPI\Framework\ServiceManager\ServiceManagerBuilder;
19
use Symfony\Component\Debug\Debug;
20
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
21
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
22
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
23
24
/**
25
 * The PPI App bootstrap class.
26
 *
27
 * This class sets various app settings, and allows you to override classes used in the bootup process.
28
 *
29
 * @author     Paul Dragoonis <[email protected]>
30
 * @author     Vítor Brandão <[email protected]>
31
 */
32
class App implements AppInterface
33
{
34
    /**
35
     * Version string.
36
     *
37
     * @var string
38
     */
39
    const VERSION = '2.2.0-DEV';
40
41
    /**
42
     * @var bool
43
     */
44
    protected $booted = false;
45
46
    /**
47
     * @var bool
48
     */
49
    protected $debug;
50
51
    /**
52
     * Application environment: "dev|development" vs "prod|production".
53
     *
54
     * @var string
55
     */
56
    protected $environment;
57
58
    /**
59
     * @var \Psr\Log\LoggerInterface
60
     */
61
    protected $logger;
62
63
    /**
64
     * Unix timestamp with microseconds.
65
     *
66
     * @var float
67
     */
68
    protected $startTime;
69
70
    /**
71
     * Configuration loader.
72
     *
73
     * @var \PPI\Framework\Config\ConfigManager
74
     */
75
    protected $configManager;
76
77
    /**
78
     * The Module Manager.
79
     *
80
     * @var \Zend\ModuleManager\ModuleManager
81
     */
82
    protected $moduleManager;
83
84
    /**
85
     * @param int $errorReportingLevel The level of error reporting you want
86
     */
87
    protected $errorReportingLevel;
88
89
    /**
90
     * @var null|array
91
     */
92
    protected $matchedRoute;
93
94
    /**
95
     * @var \PPI\Framework\Module\Controller\ControllerResolver
96
     */
97
    protected $resolver;
98
99
    /**
100
     * @var string
101
     */
102
    protected $name;
103
104
    /**
105
     * Path to the application root dir aka the "app" directory.
106
     *
107
     * @var null|string
108
     */
109
    protected $rootDir;
110
111
    /**
112
     * Service Manager.
113
     *
114
     * @var \PPI\Framework\ServiceManager\ServiceManager
115
     */
116
    protected $serviceManager;
117
118
    /**
119
     * @var ChainRouter
120
     */
121
    private $router;
122
123
    /**
124
     * App constructor.
125
     *
126
     * @param array $options
127
     */
128
    public function __construct(array $options = array())
129
    {
130
        // Default options
131
        $this->environment = isset($options['environment']) && $options['environment'] ? (string) $options['environment'] : 'prod';
132
        $this->debug = isset($options['debug']) && null !== $options['debug'] ? (boolean) $options['debug'] : false;
133
        $this->rootDir = isset($options['rootDir']) && $options['rootDir'] ? (string) $options['rootDir'] : $this->getRootDir();
134
        $this->name = isset($options['name']) && $options['name'] ? (string) $options['name'] : $this->getName();
135
136
        if ($this->debug) {
137
            $this->startTime = microtime(true);
138
            Debug::enable();
139
        } else {
140
            ini_set('display_errors', 0);
141
        }
142
    }
143
144
    public function __clone()
145
    {
146
        if ($this->debug) {
147
            $this->startTime = microtime(true);
148
        }
149
150
        $this->booted = false;
151
        $this->serviceManager = null;
152
    }
153
154
    /**
155
     * Run the boot process, load our modules and their dependencies.
156
     *
157
     * This method is automatically called by dispatch(), but you can use it
158
     * to build all services when not handling a request.
159
     *
160
     * @return $this
161
     */
162
    public function boot()
163
    {
164
        if (true === $this->booted) {
165
            return $this;
166
        }
167
168
        $this->serviceManager = $this->buildServiceManager();
169
        $this->log('debug', sprintf('Booting %s ...', $this->name));
170
171
        // Loading our Modules
172
        $this->getModuleManager()->loadModules();
173
        if ($this->debug) {
174
            $modules = $this->getModuleManager()->getModules();
175
            $this->log('debug', sprintf('All modules online (%d): "%s"', count($modules), implode('", "', $modules)));
176
        }
177
178
        // Lets get all the services our of our modules and start setting them in the ServiceManager
179
        $moduleServices = $this->serviceManager->get('ModuleDefaultListener')->getServices();
180
        foreach ($moduleServices as $key => $service) {
181
            $this->serviceManager->setFactory($key, $service);
182
        }
183
184
        $this->booted = true;
185
        if ($this->debug) {
186
            $this->log('debug', sprintf('%s has booted (in %.3f secs)', $this->name, microtime(true) - $this->startTime));
187
        }
188
189
        return $this;
190
    }
191
192
    /**
193
     * Run the application and send the response.
194
     *
195
     * @param HttpRequest|null  $request
196
     * @param HttpResponse|null $response
197
     *
198
     * @throws \Exception
199
     *
200
     * @return HttpResponse
201
     */
202
    public function run(HttpRequest $request = null, HttpResponse $response = null)
203
    {
204
        if (false === $this->booted) {
205
            $this->boot();
206
        }
207
208
        if (null === $request) {
209
            $request = HttpRequest::createFromGlobals();
210
        }
211
212
        if (null === $response) {
213
            $response = new HttpResponse();
214
        }
215
216
        $response = $this->dispatch($request, $response);
0 ignored issues
show
Compatibility introduced by
$request of type object<Symfony\Component\HttpFoundation\Request> is not a sub-type of object<PPI\Framework\Http\Request>. It seems like you assume a child class of the class Symfony\Component\HttpFoundation\Request to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
217
        $response->send();
218
219
        return $response;
220
    }
221
222
    /**
223
     * Decide on a route to use and dispatch our module's controller action.
224
     *
225
     * @param HttpRequest  $request
226
     * @param HttpResponse $response
227
     *
228
     * @throws \Exception
229
     *
230
     * @return HttpResponse
231
     */
232
    public function dispatch(HttpRequest $request, HttpResponse $response)
233
    {
234
        if (false === $this->booted) {
235
            $this->boot();
236
        }
237
238
        // Routing
239
        $routeParams = $this->handleRouting($request);
240
        $request->attributes->add($routeParams);
241
242
        // Resolve our Controller
243
        $resolver = $this->serviceManager->get('ControllerResolver');
244
        if (false === $controller = $resolver->getController($request)) {
245
            throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s".', $request->getPathInfo()));
246
        }
247
248
        $controllerArguments = $resolver->getArguments($request, $controller);
249
250
        $result = call_user_func_array(
251
            $controller,
252
            $controllerArguments
253
        );
254
255
        if ($result === null) {
256
            throw new \Exception('Your action returned null. It must always return something');
257
        } elseif (is_string($result)) {
258
            $response->setContent($result);
259
        } elseif ($result instanceof SymfonyResponse || $response instanceof HttpResponse) {
260
            $response = $result;
261
        } else {
262
            throw new \Exception('Invalid response type returned from controller');
263
        }
264
265
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (object|integer|double|null|array|boolean) is incompatible with the return type documented by PPI\Framework\App::dispatch of type PPI\Framework\Http\Response|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
266
    }
267
268
    /**
269
     * Gets the name of the application.
270
     *
271
     * @return string The application name
272
     *
273
     * @api
274
     */
275
    public function getName()
276
    {
277
        if (null === $this->name) {
278
            $this->name = preg_replace('/[^a-zA-Z0-9_]+/', '', basename($this->rootDir));
279
        }
280
281
        return $this->name;
282
    }
283
284
    /**
285
     * Gets the version of the application.
286
     *
287
     * @return string The application version
288
     *
289
     * @api
290
     */
291
    public function getVersion()
292
    {
293
        return self::VERSION;
294
    }
295
296
    /**
297
     * Get the environment mode the application is in.
298
     *
299
     * @return string The current environment
300
     *
301
     * @api
302
     */
303
    public function getEnvironment()
304
    {
305
        return $this->environment;
306
    }
307
308
    /**
309
     * @param $env
310
     *
311
     * @return bool
312
     */
313
    public function isEnvironment($env)
314
    {
315
        if ('development' == $env) {
316
            $env = 'dev';
317
        } elseif ('production' == $env) {
318
            $env = 'prod';
319
        }
320
321
        return $this->getEnvironment() == $env;
322
    }
323
324
    /**
325
     * Checks if debug mode is enabled.
326
     *
327
     * @return bool true if debug mode is enabled, false otherwise
328
     *
329
     * @api
330
     */
331
    public function isDebug()
332
    {
333
        return $this->debug;
334
    }
335
336
    /**
337
     * Gets the application root dir.
338
     *
339
     * @return string The application root dir
340
     *
341
     * @api
342
     */
343
    public function getRootDir()
344
    {
345
        if (null === $this->rootDir) {
346
            $this->rootDir = realpath(getcwd() . '/app');
347
        }
348
349
        return $this->rootDir;
350
    }
351
352
    /**
353
     * Get the service manager.
354
     *
355
     * @return ServiceManager
356
     */
357
    public function getServiceManager()
358
    {
359
        return $this->serviceManager;
360
    }
361
362
    /**
363
     * @note Added for compatibility with Symfony's HttpKernel\Kernel.
364
     *
365
     * @return null|ServiceManager
366
     */
367
    public function getContainer()
368
    {
369
        return $this->serviceManager;
370
    }
371
372
    /**
373
     * Returns the Module Manager.
374
     *
375
     * @return \Zend\ModuleManager\ModuleManager
376
     */
377
    public function getModuleManager()
378
    {
379
        if (null === $this->moduleManager) {
380
            $this->moduleManager = $this->serviceManager->get('ModuleManager');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->serviceManager->get('ModuleManager') can also be of type array. However, the property $moduleManager is declared as type object<Zend\ModuleManager\ModuleManager>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
381
        }
382
383
        return $this->moduleManager;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->moduleManager; of type object|array adds the type array to the return on line 383 which is incompatible with the return type documented by PPI\Framework\App::getModuleManager of type Zend\ModuleManager\ModuleManager.
Loading history...
384
    }
385
386
    /**
387
     * Get an array of the loaded modules.
388
     *
389
     * @return array An array of Module objects, keyed by module name
390
     */
391
    public function getModules()
392
    {
393
        return $this->getModuleManager()->getLoadedModules(true);
394
    }
395
396
    /**
397
     * @see PPI\Framework\Module\ModuleManager::locateResource()
398
     *
399
     * @param string $name  A resource name to locate
400
     * @param string $dir   A directory where to look for the resource first
401
     * @param bool   $first Whether to return the first path or paths for all matching bundles
402
     *
403
     * @throws \InvalidArgumentException if the file cannot be found or the name is not valid
404
     * @throws \RuntimeException         if the name contains invalid/unsafe
405
     * @throws \RuntimeException         if a custom resource is hidden by a resource in a derived bundle
406
     *
407
     * @return string|array The absolute path of the resource or an array if $first is false
408
     */
409
    public function locateResource($name, $dir = null, $first = true)
410
    {
411
        return $this->getModuleManager()->locateResource($name, $dir, $first);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Zend\ModuleManager\ModuleManager as the method locateResource() does only exist in the following sub-classes of Zend\ModuleManager\ModuleManager: PPI\Framework\Module\ModuleManager. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
412
    }
413
414
    /**
415
     * Gets the request start time (not available if debug is disabled).
416
     *
417
     * @return int The request start timestamp
418
     *
419
     * @api
420
     */
421
    public function getStartTime()
422
    {
423
        return $this->debug ? $this->startTime : -INF;
424
    }
425
426
    /**
427
     * Gets the cache directory.
428
     *
429
     * @return string The cache directory
430
     *
431
     * @api
432
     */
433
    public function getCacheDir()
434
    {
435
        return $this->rootDir . '/cache/' . $this->environment;
436
    }
437
438
    /**
439
     * Gets the log directory.
440
     *
441
     * @return string The log directory
442
     *
443
     * @api
444
     */
445
    public function getLogDir()
446
    {
447
        return $this->rootDir . '/logs';
448
    }
449
450
    /**
451
     * Gets the charset of the application.
452
     *
453
     * @return string The charset
454
     *
455
     * @api
456
     */
457
    public function getCharset()
458
    {
459
        return 'UTF-8';
460
    }
461
462
    /**
463
     * Returns a ConfigManager instance.
464
     *
465
     * @return \PPI\Framework\Config\ConfigManager
466
     */
467
    public function getConfigManager()
468
    {
469
        if (null === $this->configManager) {
470
            $cachePath = $this->getCacheDir() . '/application-config-cache.' . $this->getName() . '.php';
471
            $this->configManager = new ConfigManager($cachePath, !$this->debug, $this->rootDir . '/config');
472
        }
473
474
        return $this->configManager;
475
    }
476
477
    /**
478
     * Loads a configuration file or PHP array.
479
     *
480
     * @param  $resource
481
     * @param null $type
482
     *
483
     * @return App The current instance
484
     */
485
    public function loadConfig($resource, $type = null)
486
    {
487
        $this->getConfigManager()->addConfig($resource, $type);
488
489
        return $this;
490
    }
491
492
    /**
493
     * Returns the application configuration.
494
     *
495
     * @throws \RuntimeException
496
     *
497
     * @return array|object
498
     */
499
    public function getConfig()
500
    {
501
        if (!$this->booted) {
502
            throw new \RuntimeException('The "Config" service is only available after the App boot()');
503
        }
504
505
        return $this->serviceManager->get('Config');
506
    }
507
508
    public function serialize()
509
    {
510
        return serialize(array($this->environment, $this->debug));
511
    }
512
513
    public function unserialize($data)
514
    {
515
        list($environment, $debug) = unserialize($data);
516
517
        $this->__construct($environment, $debug);
0 ignored issues
show
Unused Code introduced by
The call to App::__construct() has too many arguments starting with $debug.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
518
    }
519
520
    /**
521
     * Returns the application parameters.
522
     *
523
     * @return array An array of application parameters
524
     */
525
    protected function getAppParameters()
526
    {
527
        return array_merge(
528
            array(
529
                'app.root_dir' => $this->rootDir,
530
                'app.environment' => $this->environment,
531
                'app.debug' => $this->debug,
532
                'app.name' => $this->name,
533
                'app.cache_dir' => $this->getCacheDir(),
534
                'app.logs_dir' => $this->getLogDir(),
535
                'app.charset' => $this->getCharset(),
536
            ),
537
            $this->getEnvParameters()
538
        );
539
    }
540
541
    /**
542
     * Gets the environment parameters.
543
     *
544
     * Only the parameters starting with "PPI__" are considered.
545
     *
546
     * @return array An array of parameters
547
     */
548
    protected function getEnvParameters()
0 ignored issues
show
Coding Style introduced by
getEnvParameters uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
549
    {
550
        $parameters = array();
551
        foreach ($_SERVER as $key => $value) {
552
            if (0 === strpos($key, 'PPI__')) {
553
                $parameters[strtolower(str_replace('__', '.', substr($key, 5)))] = $value;
554
            }
555
        }
556
557
        return $parameters;
558
    }
559
560
    /**
561
     * Creates and initializes a ServiceManager instance.
562
     *
563
     * @return ServiceManager The compiled service manager
564
     */
565
    protected function buildServiceManager()
566
    {
567
        // ServiceManager creation
568
        $serviceManager = new ServiceManagerBuilder($this->getConfigManager()->getMergedConfig());
569
        $serviceManager->build($this->getAppParameters());
570
        $serviceManager->set('app', $this);
571
572
        return $serviceManager;
573
    }
574
575
    /**
576
     * Perform the matching of a route and return a set of routing parameters if a valid one is found.
577
     * Otherwise exceptions get thrown.
578
     *
579
     * @param HttpRequest $request
580
     *
581
     * @throws \Exception
582
     *
583
     * @return array
584
     */
585
    protected function handleRouting(HttpRequest $request)
586
    {
587
        $this->router = $this->serviceManager->get('Router');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->serviceManager->get('Router') can also be of type array. However, the property $router is declared as type object<PPI\Framework\Router\ChainRouter>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
588
        $this->router->warmUp($this->getCacheDir());
589
590
        try {
591
            // Lets load up our router and match the appropriate route
592
            $parameters = $this->router->matchRequest($request);
593 View Code Duplication
            if (!empty($parameters)) {
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...
594
                if (null !== $this->logger) {
595
                    $this->logger->info(sprintf('Matched route "%s" (parameters: %s)', $parameters['_route'], $this->router->parametersToString($parameters)));
596
                }
597
            }
598
        } catch (ResourceNotFoundException $e) {
599
            $routeUri = $this->router->generate('Framework_404');
600
            $parameters = $this->router->matchRequest($request::create($routeUri));
601
        } catch (\Exception $e) {
602
            throw $e;
603
        }
604
605
        $parameters['_route_params'] = $parameters;
606
607
        return $parameters;
608
    }
609
610
    /**
611
     * Logs with an arbitrary level.
612
     *
613
     * @param mixed  $level
614
     * @param string $message
615
     * @param array  $context
616
     */
617
    protected function log($level, $message, array $context = array())
618
    {
619
        if (null === $this->logger && $this->getServiceManager()->has('logger')) {
620
            $this->logger = $this->getServiceManager()->get('logger');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getServiceManager()->get('logger') can also be of type array. However, the property $logger is declared as type object<Psr\Log\LoggerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
621
        }
622
623
        if ($this->logger) {
624
            $this->logger->log($level, $message, $context);
625
        }
626
    }
627
}
628