| Total Complexity | 40 | 
| Total Lines | 346 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 26 | class Container implements ContainerInterface | ||
| 27 | { | ||
| 28 | /** | ||
| 29 | * @var Definition[] object definitions indexed by their types | ||
| 30 | */ | ||
| 31 | private $definitions = []; | ||
| 32 | /** | ||
| 33 | * @var ReflectionClass[] cached ReflectionClass objects indexed by class/interface names | ||
| 34 | */ | ||
| 35 | private $reflections = []; | ||
|  | |||
| 36 | /** | ||
| 37 | * @var array used to collect ids instantiated during build | ||
| 38 | * to detect circular references | ||
| 39 | */ | ||
| 40 | private $building = []; | ||
| 41 | /** | ||
| 42 | * @var contracts\DeferredServiceProvider[]|\SplObjectStorage list of providers | ||
| 43 | * deferred to register till their services would be requested | ||
| 44 | */ | ||
| 45 | private $deferredProviders; | ||
| 46 | /** | ||
| 47 | * @var Injector injector with this container. | ||
| 48 | */ | ||
| 49 | protected $injector; | ||
| 50 | |||
| 51 | /** | ||
| 52 | * @var object[] | ||
| 53 | */ | ||
| 54 | private $instances; | ||
| 55 | |||
| 56 | /** @var ?ContainerInterface */ | ||
| 57 | private $rootContainer; | ||
| 58 | /** | ||
| 59 | * Container constructor. | ||
| 60 | * | ||
| 61 | * @param array $definitions | ||
| 62 | * @param ServiceProvider[] $providers | ||
| 63 | * | ||
| 64 | * @throws InvalidConfigException | ||
| 65 | * @throws NotInstantiableException | ||
| 66 | */ | ||
| 67 | public function __construct( | ||
| 68 | array $definitions = [], | ||
| 69 | array $providers = [], | ||
| 70 | ?ContainerInterface $rootContainer = null | ||
| 71 |     ) { | ||
| 72 | $this->rootContainer = $rootContainer; | ||
| 73 | $this->setMultiple($definitions); | ||
| 74 | $this->deferredProviders = new SplObjectStorage(); | ||
| 75 |         foreach ($providers as $provider) { | ||
| 76 | $this->addProvider($provider); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | /** | ||
| 81 | * Returns an instance by either interface name or alias. | ||
| 82 | * | ||
| 83 | * Same instance of the class will be returned each time this method is called. | ||
| 84 | * | ||
| 85 | * @param string|Reference $id the interface or an alias name that was previously registered via [[set()]]. | ||
| 86 | * @return object an instance of the requested interface. | ||
| 87 | * @throws CircularReferenceException | ||
| 88 | * @throws InvalidConfigException | ||
| 89 | * @throws NotFoundException | ||
| 90 | * @throws NotInstantiableException | ||
| 91 | */ | ||
| 92 | public function get($id) | ||
| 93 |     { | ||
| 94 | return $this->getWithParams($id); | ||
| 95 | } | ||
| 96 | |||
| 97 | public function getWithParams($id, array $params = []) | ||
| 98 |     { | ||
| 99 | $id = $this->getId($id); | ||
| 100 |         if (!isset($this->instances[$id])) { | ||
| 101 | $object = $this->build($id, $params); | ||
| 102 | $this->instances[$id] = $object; | ||
| 103 | $this->initObject($object); | ||
| 104 | } | ||
| 105 | |||
| 106 | return $this->instances[$id]; | ||
| 107 | } | ||
| 108 | |||
| 109 | public function getId($id): string | ||
| 110 |     { | ||
| 111 | return is_string($id) ? $id : $id->getId(); | ||
| 112 | } | ||
| 113 | |||
| 114 | /** | ||
| 115 | * Returns normalized definition for a given id | ||
| 116 | */ | ||
| 117 | public function getDefinition(string $id): ?Definition | ||
| 118 |     { | ||
| 119 | return $this->definitions[$id] ?? null; | ||
| 120 | } | ||
| 121 | |||
| 122 | /** | ||
| 123 | * Creates new instance by either interface name or alias. | ||
| 124 | * | ||
| 125 | * @param string $id the interface or an alias name that was previously registered via [[set()]]. | ||
| 126 | * @param array $config | ||
| 127 | * @return object new built instance of the specified class. | ||
| 128 | * @throws CircularReferenceException | ||
| 129 | * @throws InvalidConfigException | ||
| 130 | * @throws NotFoundException if there is nothing registered with alias or interface specified | ||
| 131 | * @throws NotInstantiableException | ||
| 132 | * @internal | ||
| 133 | */ | ||
| 134 | protected function build(string $id, array $params = []) | ||
| 135 |     { | ||
| 136 |         if (isset($this->building[$id])) { | ||
| 137 |             if ($id === 'container') { | ||
| 138 | return $this; | ||
| 139 | } | ||
| 140 | throw new CircularReferenceException(sprintf( | ||
| 141 | 'Circular reference to "%s" detected while building: %s', | ||
| 142 | $id, | ||
| 143 |                 implode(',', array_keys($this->building)) | ||
| 144 | )); | ||
| 145 | } | ||
| 146 | |||
| 147 | $this->building[$id] = 1; | ||
| 148 | $this->registerProviderIfDeferredFor($id); | ||
| 149 | $object = $this->buildInternal($id, $params); | ||
| 150 | unset($this->building[$id]); | ||
| 151 | |||
| 152 | return $object; | ||
| 153 | } | ||
| 154 | |||
| 155 | protected function buildInternal(string $id, array $params = []) | ||
| 156 |     { | ||
| 157 |         if (!isset($this->definitions[$id])) { | ||
| 158 |             if (isset($this->rootContainer)) { | ||
| 159 | return $this->rootContainer->getWithParams($id, $params); | ||
| 160 | } | ||
| 161 | $res = $this->buildPrimitive($id, $params); | ||
| 162 |             if ($res !== null) { | ||
| 163 | return $res; | ||
| 164 | } | ||
| 165 |             throw new NotFoundException("No definition for $id"); | ||
| 166 | } | ||
| 167 | |||
| 168 | $definition = $this->definitions[$id]; | ||
| 169 | |||
| 170 | return $definition->resolve($this, $params); | ||
| 171 | } | ||
| 172 | |||
| 173 | protected function buildPrimitive(string $class, array $params = []) | ||
| 174 |     { | ||
| 175 |         if ($class === ContainerInterface::class) { | ||
| 176 | return $this; | ||
| 177 | } | ||
| 178 |         if (class_exists($class)) { | ||
| 179 | $definition = ArrayDefinition::fromClassName($class); | ||
| 180 | |||
| 181 | return $definition->resolve($this, $params); | ||
| 182 | } | ||
| 183 | |||
| 184 | return null; | ||
| 185 | } | ||
| 186 | |||
| 187 | /** | ||
| 188 |      * Register providers from {@link deferredProviders} if they provide | ||
| 189 | * definition for given identifier. | ||
| 190 | * | ||
| 191 | * @param string $id class or identifier of a service. | ||
| 192 | */ | ||
| 193 | private function registerProviderIfDeferredFor(string $id): void | ||
| 194 |     { | ||
| 195 | $providers = $this->deferredProviders; | ||
| 196 | |||
| 197 |         foreach ($providers as $provider) { | ||
| 198 |             if ($provider->hasDefinitionFor($id)) { | ||
| 199 | $provider->register($this); | ||
| 200 | |||
| 201 | // provider should be removed after registration to not be registered again | ||
| 202 | $providers->detach($provider); | ||
| 203 | } | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | /** | ||
| 208 | * Sets a definition to the container. Definition may be defined multiple ways. | ||
| 209 | * @see `Normalizer::normalize()` | ||
| 210 | * @param string $id | ||
| 211 | * @param mixed $definition | ||
| 212 | */ | ||
| 213 | public function set(string $id, $definition): void | ||
| 214 |     { | ||
| 215 | $this->instances[$id] = null; | ||
| 216 | $this->definitions[$id] = Normalizer::normalize($definition, $id); | ||
| 217 | } | ||
| 218 | |||
| 219 | /** | ||
| 220 | * Sets multiple definitions at once. | ||
| 221 | * @param array $config definitions indexed by their ids | ||
| 222 | */ | ||
| 223 | public function setMultiple(array $config): void | ||
| 224 |     { | ||
| 225 |         foreach ($config as $id => $definition) { | ||
| 226 | $this->set($id, $definition); | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * Returns a value indicating whether the container has the definition of the specified name. | ||
| 232 | * @param string $id class name, interface name or alias name | ||
| 233 | * @return bool whether the container is able to provide instance of class specified. | ||
| 234 | * @throws CircularReferenceException | ||
| 235 | * @see set() | ||
| 236 | */ | ||
| 237 | public function has($id): bool | ||
| 238 |     { | ||
| 239 | return isset($this->definitions[$id]); | ||
| 240 | } | ||
| 241 | |||
| 242 | /** | ||
| 243 | * Does after build object initialization. | ||
| 244 | * At the moment only `init()` if class implements Initiable interface. | ||
| 245 | * | ||
| 246 | * @param object $object | ||
| 247 | * @return object | ||
| 248 | */ | ||
| 249 | protected function initObject($object) | ||
| 250 |     { | ||
| 251 |         if ($object instanceof Initiable) { | ||
| 252 | $object->init(); | ||
| 253 | } | ||
| 254 | |||
| 255 | return $object; | ||
| 256 | } | ||
| 257 | |||
| 258 | /** | ||
| 259 | * Resolves dependencies by replacing them with the actual object instances. | ||
| 260 | * @param Definition[] $dependencies the dependencies | ||
| 261 | * @return array the resolved dependencies | ||
| 262 | * @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled. | ||
| 263 | */ | ||
| 264 | public function resolveDependencies(array $dependencies): array | ||
| 265 |     { | ||
| 266 | $result = []; | ||
| 267 | /** @var Definition $dependency */ | ||
| 268 |         foreach ($dependencies as $dependency) { | ||
| 269 | $result[] = $this->resolve($dependency); | ||
| 270 | } | ||
| 271 | |||
| 272 | return $result; | ||
| 273 | } | ||
| 274 | |||
| 275 | /** | ||
| 276 | * This function resolves a dependency recursively, checking for loops. | ||
| 277 | * TODO add checking for loops | ||
| 278 | * @param Definition $dependency | ||
| 279 | * @return Definition | ||
| 280 | */ | ||
| 281 | public function resolve(Definition $dependency) | ||
| 282 |     { | ||
| 283 |         while ($dependency instanceof Definition) { | ||
| 284 | $dependency = $dependency->resolve($this->getRootContainer()); | ||
| 285 | } | ||
| 286 | return $dependency; | ||
| 287 | } | ||
| 288 | |||
| 289 | /** | ||
| 290 | * Adds service provider to the container. Unless service provider is deferred | ||
| 291 | * it would be immediately registered. | ||
| 292 | * | ||
| 293 | * @param string|array $providerDefinition | ||
| 294 | * | ||
| 295 | * @throws InvalidConfigException | ||
| 296 | * @throws NotInstantiableException | ||
| 297 | * @see ServiceProvider | ||
| 298 | * @see DeferredServiceProvider | ||
| 299 | */ | ||
| 300 | public function addProvider($providerDefinition): void | ||
| 301 |     { | ||
| 302 | $provider = $this->buildProvider($providerDefinition); | ||
| 303 | |||
| 304 |         if ($provider instanceof DeferredServiceProvider) { | ||
| 305 | $this->deferredProviders->attach($provider); | ||
| 306 |         } else { | ||
| 307 | $provider->register($this); | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 | /** | ||
| 312 | * Builds service provider by definition. | ||
| 313 | * | ||
| 314 | * @param string|array $providerDefinition class name or definition of provider. | ||
| 315 | * @return ServiceProvider instance of service provider; | ||
| 316 | * | ||
| 317 | * @throws InvalidConfigException | ||
| 318 | * @throws NotInstantiableException | ||
| 319 | */ | ||
| 320 | private function buildProvider($providerDefinition): ServiceProvider | ||
| 321 |     { | ||
| 322 | $provider = Normalizer::normalize($providerDefinition)->resolve($this); | ||
| 323 |         if (!($provider instanceof ServiceProvider)) { | ||
| 324 | throw new InvalidConfigException( | ||
| 325 | 'Service provider should be an instance of ' . ServiceProvider::class | ||
| 326 | ); | ||
| 327 | } | ||
| 328 | |||
| 329 | return $provider; | ||
| 330 | } | ||
| 331 | |||
| 332 | /** | ||
| 333 | * Returns injector. | ||
| 334 | * | ||
| 335 | * @return Injector | ||
| 336 | */ | ||
| 337 | public function getInjector(): Injector | ||
| 338 |     { | ||
| 339 |         if ($this->injector === null) { | ||
| 340 | $this->injector = new Injector($this); | ||
| 341 | } | ||
| 342 | |||
| 343 | return $this->injector; | ||
| 344 | } | ||
| 345 | |||
| 346 | public function getRootContainer(): ContainerInterface | ||
| 349 | } | ||
| 350 | |||
| 351 | /** | ||
| 352 | * Returns a value indicating whether the container has already instantiated | ||
| 353 | * instance of the specified name. | ||
| 354 | * @param string|Reference $id class name, interface name or alias name | ||
| 355 | * @return bool whether the container has instance of class specified. | ||
| 356 | * @throws CircularReferenceException | ||
| 357 | */ | ||
| 358 | public function hasInstance($id): bool | ||
| 363 | } | ||
| 364 | |||
| 365 | /** | ||
| 366 | * Returns all instances set in container | ||
| 367 | * @return array list of instance | ||
| 368 | */ | ||
| 369 | public function getInstances() : array | ||
| 370 |     { | ||
| 371 | return $this->instances; | ||
| 372 | } | ||
| 373 | } | ||
| 374 |