Container.php 32 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
<?php
/**
 * The Dependency Injection container.
 *
 * @package lucatume\DI52
 *
 * @license GPL-3.0
 * Modified by learndash on 06-June-2023 using Strauss.
 * @see https://github.com/BrianHenryIE/strauss
 */

namespace StellarWP\Learndash\lucatume\DI52;

use ArrayAccess;
use Closure;
use Exception;
use StellarWP\Learndash\lucatume\DI52\Builders\BuilderInterface;
use StellarWP\Learndash\lucatume\DI52\Builders\ValueBuilder;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReturnTypeWillChange;
use Throwable;
use function spl_object_hash;

/**
 * Class Container
 *
 * @package lucatume\DI52
 * @implements ArrayAccess<string,object>
 */
class Container implements ArrayAccess, ContainerInterface
{
    const EXCEPTION_MASK_NONE = 0;
    const EXCEPTION_MASK_MESSAGE = 1;
    const EXCEPTION_MASK_FILE_LINE = 2;

    /**
     * An array cache to store the results of the class exists checks.
     *
     * @var array<string,bool>
     */
    protected $classIsInstantiatableCache = [];
    /**
     * A cache of what methods are static and what are not.
     *
     * @var array<string,bool>
     */
    protected $isStaticMethodCache = [];
    /**
     * A list of bound and resolved singletons.
     *
     * @var array<string,bool>
     */
    protected $singletons = [];
    /**
     * @var array<ServiceProvider>
     */
    protected $deferred = [];
    /**
     * @var array<string,array<string|object|callable>>
     */
    protected $tags = [];
    /**
     * @var array<ServiceProvider>
     */
    protected $bootable = [];
    /**
     * @var string
     */
    protected $whenClass;
    /**
     * @var string
     */
    protected $needsClass;
    /**
     * A map from class name and static methods to the built callback.
     *
     * @var array<string,Closure>
     */
    protected $callbacks = [];
    /**
     * @var Builders\Resolver
     */
    protected $resolver;
    /**
     * @var Builders\Factory
     */
    protected $builders;
    /**
     * What kind of masking should be applied to throwables catched by the container during resolution.
     *
     * @var int
     */
    private $maskThrowables = self::EXCEPTION_MASK_MESSAGE | self::EXCEPTION_MASK_FILE_LINE;

    /**
     * Container constructor.
     *
     * @param false $resolveUnboundAsSingletons Whether unbound classes should be resolved as singletons by default,
     *                                          or not.
     */
    public function __construct($resolveUnboundAsSingletons = false)
    {
        $this->resolver = new Builders\Resolver($resolveUnboundAsSingletons);
        $this->builders = new Builders\Factory($this, $this->resolver);
        $this->singleton(Container::class, $this);
    }

    /**
     * Sets a variable on the container.
     *
     * @param string $key   The alias the container will use to reference the variable.
     * @param mixed  $value The variable value.
     *
     * @return void The method does not return any value.
     */
    public function setVar($key, $value)
    {
        $this->resolver->bind($key, ValueBuilder::of($value));
    }

    /**
     * Sets a variable on the container using the ArrayAccess API.
     *
     * When using the container as an array bindings will be bound as singletons.
     * These are equivalent: `$container->singleton('foo','ClassOne');`, `$container['foo'] = 'ClassOne';`.
     *
     * @param string $offset The alias the container will use to reference the variable.
     * @param mixed  $value  The variable value.
     *
     * @return void This method does not return any value.
     *
     * @throws ContainerException If the closure building fails.
     */
    #[ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        $this->singleton($offset, $value);
    }

    /**
     * Binds an interface a class or a string slug to an implementation and will always return the same instance.
     *
     * @param string             $id                A class or interface fully qualified name or a string slug.
     * @param mixed              $implementation    The implementation that should be bound to the alias(es); can be a
     *                                              class name, an object or a closure.
     * @param array<string>|null $afterBuildMethods An array of methods that should be called on the built
     *                                              implementation after resolving it.
     *
     * @return void This method does not return any value.
     * @throws ContainerException If there's any issue reflecting on the class, interface or the implementation.
     */
    public function singleton($id, $implementation = null, array $afterBuildMethods = null)
    {
        if ($implementation === null) {
            $implementation = $id;
        }

        $this->resolver->singleton($id, $this->builders->getBuilder($id, $implementation, $afterBuildMethods));
    }

    /**
     * Returns a variable stored in the container.
     *
     * If the variable is a binding then the binding will be resolved before returning it.
     *
     * @param string     $key     The alias of the variable or binding to fetch.
     * @param mixed|null $default A default value to return if the variable is not set in the container.
     *
     * @return mixed The variable value or the resolved binding.
     * @throws ContainerException If there's an issue resolving the variable.
     *
     * @see Container::get()
     */
    public function getVar($key, $default = null)
    {
        if ($this->resolver->isBound($key)) {
            return $this->resolver->resolve($key);
        }

        return $default;
    }

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $offset Identifier of the entry to look for.
     *
     * @return mixed The entry for an id.
     *
     * @return mixed The value for the offset.
     *
     * @throws ContainerException Error while retrieving the entry.
     * @throws NotFoundException  No entry was found for **this** identifier.
     */
    #[ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id A fully qualified class or interface name or an already built object.
     *
     * @return mixed The entry for an id.
     *
     * @throws ContainerException Error while retrieving the entry.
     */
    public function get($id)
    {
        try {
            return $this->resolver->resolve($id, [$id]);
        } catch (Throwable $throwable) {
            throw $this->castThrown($throwable, $id);
            // @codeCoverageIgnoreStart
        } catch (Exception $exception) { // @phan-suppress-current-line PhanUnreachableCatch @phpstan-ignore-line
            throw $this->castThrown($exception, $id);
            // @codeCoverageIgnoreEnd
        }
    }

    /**
     * Builds an instance of the exception with a pretty message.
     *
     * @param Exception|Throwable $thrown The exception to cast.
     * @param string|object $id The top identifier the containe was attempting to build, or object.
     *
     * @return ContainerException|Exception|Throwable The cast exception.
     */
    private function castThrown($thrown, $id)
    {
        if ($this->maskThrowables === self::EXCEPTION_MASK_NONE) {
            return $thrown;
        }

        return ContainerException::fromThrowable(
            $id,
            $thrown,
            $this->maskThrowables,
            $this->resolver->getBuildLine()
        );
    }

    /**
     * Returns an instance of the class or object bound to an interface, class  or string slug if any, else it will try
     * to automagically resolve the object to a usable instance.
     *
     * If the implementation has been bound as singleton using the `singleton` method
     * or the ArrayAccess API then the implementation will be resolved just on the first request.
     *
     * @param string $id A fully qualified class or interface name or an already built object.
     *
     * @return mixed
     * @throws ContainerException If the target of the make is not bound and is not a valid,
     *                                              concrete, class name or there's any issue making the target.
     */
    public function make($id)
    {
        return $this->get($id);
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `$container[$id]` returning true does not mean that `$container[$id]` will not throw an exception.
     * It does however mean that `$container[$id]` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $offset An offset to check for.
     *
     * @return boolean true on success or false on failure.
     */
    #[ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return $this->has($offset);
    }

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool Whether the container contains a binding for an id or not.
     */
    public function has($id)
    {
        return $this->resolver->isBound($id) || class_exists($id);
    }

    /**
     * Tags an array of implementations bindings for later retrieval.
     *
     * The implementations can also reference interfaces, classes or string slugs.
     * Example:
     *
     *        $container->tag(['Posts', 'Users', 'Comments'], 'endpoints');
     *
     * @param array<string|callable|object> $implementationsArray The ids, class names or objects to apply the tag to.
     * @param string                        $tag                  The tag to apply.
     *
     * @return void This method does not return any value.
     * @see Container::tagged()
     *
     */
    public function tag(array $implementationsArray, $tag)
    {
        $this->tags[$tag] = $implementationsArray;
    }

    /**
     * Retrieves an array of bound implementations resolving them.
     *
     * The array of implementations should be bound using the `tag` method:
     *
     *        $container->tag(['Posts', 'Users', 'Comments'], 'endpoints');
     *        foreach($container->tagged('endpoints') as $endpoint){
     *            $endpoint->register();
     *        }
     *
     * @param string $tag The tag to return the tagged values for.
     *
     * @return array<mixed> An array of resolved bound implementations.
     * @throws NotFoundException If nothing is tagged with the tag.
     * @throws ContainerException If one of the bindings is not of the correct type.
     * @see Container::tag()
     */
    public function tagged($tag)
    {
        if (!$this->hasTag($tag)) {
            throw new NotFoundException("Nothing is tagged as '{$tag}'");
        }

        return array_map(
            function ($id) {
                if (is_string($id)) {
                    return $this->get($id);
                }
                return $this->builders->getBuilder($id)->build();
            },
            $this->tags[$tag]
        );
    }

    /**
     * Checks whether a tag group exists in the container.
     *
     * @param string $tag
     *
     * @return bool
     * @see Container::tag()
     *
     */
    public function hasTag($tag)
    {
        return isset($this->tags[$tag]);
    }

    /**
     * A wrapper around the `class_exists` function to capture and handle possible fatal errors on PHP 7.0+.
     *
     * @param string $class The class name to check.
     *
     * @return bool Whether the class exists or not.
     *
     * @throws ContainerException|ReflectionException If the class has syntax or other errors preventing its load.
     */
    protected function classIsInstantiable($class)
    {
        if (isset($this->classIsInstantiatableCache[$class])) {
            return $this->classIsInstantiatableCache[$class];
        }

        // @codeCoverageIgnoreStart
        if (PHP_VERSION_ID < 70000) {
            $isInstantiatable = $this->checkClassIsInstantiatable($class);
            $this->classIsInstantiatableCache[$class] = $isInstantiatable;
            return $isInstantiatable;
        }
        // @codeCoverageIgnoreEnd

        // PHP 7.0+ allows handling fatal errors; x_exists will trigger auto-loading, that might result in an error.
        try {
            $isInstantiatable = $this->checkClassIsInstantiatable($class);
            $this->classIsInstantiatableCache[$class] = $isInstantiatable;
            return $isInstantiatable;
        } catch (Throwable $e) {
            $this->classIsInstantiatableCache[$class] = false;
            throw new ContainerException($e->getMessage());
        }
    }

    /**
     * Checks a class, interface or trait exists.
     *
     * @param string $class The class, interface or trait to check.
     *
     * @return bool Whether the class, interface or trait exists or not.
     * @throws ReflectionException If the class should be checked for concreteness and it does not exist.
     */
    protected function checkClassIsInstantiatable($class)
    {
        $exists = class_exists($class);
        if (!$exists) {
            return false;
        }
        $classReflection = new ReflectionClass($class);
        if ($classReflection->isAbstract()) {
            return false;
        }
        $constructor = $classReflection->getConstructor();
        if ($constructor === null) {
            return true;
        }
        return $constructor->isPublic();
    }

    /**
     * Registers a service provider implementation.
     *
     * The `register` method will be called immediately on the service provider.
     *
     * If the provider overloads the  `isDeferred` method returning a truthy value then the `register` method will be
     * called only if one of the implementations provided by the provider is requested. The container defines which
     * implementations is offering overloading the `provides` method; the method should return an array of provided
     * implementations.
     *
     * If a provider overloads the `boot` method that method will be called when the `boot` method is called on the
     * container itself.
     *
     * @param string $serviceProviderClass The fully-qualified Service Provider class name.
     * @param string ...$alias             A list of aliases the provider should be registered with.
     * @return void This method does not return any value.
     * @throws ContainerException If the Service Provider is not correctly configured or there's an issue
     *                                     reflecting on it.
     * @see ServiceProvider::register()
     * @see ServiceProvider::isDeferred()
     * @see ServiceProvider::provides()
     * @see Container::getProvider()
     * @see ServiceProvider::boot()
     */
    public function register($serviceProviderClass, ...$alias)
    {
        /** @var ServiceProvider $provider */
        $provider = $this->get($serviceProviderClass);
        if (!$provider->isDeferred()) {
            $provider->register();
        } else {
            $provided = $provider->provides();
            if (!is_array($provided) || count($provided) === 0) {
                throw new ContainerException(
                    "Service provider '{$serviceProviderClass}' is marked as deferred" .
                    " but is not providing any implementation."
                );
            }
            foreach ($provided as $id) {
                $this->resolver->bind(
                    $id,
                    $this->builders->getBuilder($this->getDeferredProviderMakeClosure($provider, $id))
                );
            }
        }

        try {
            $bootMethod = new ReflectionMethod($provider, 'boot');
        } catch (ReflectionException $e) {
            throw new ContainerException('Could not reflect on the provider boot method.');
        }

        $requiresBoot = ($bootMethod->getDeclaringClass()->getName() === get_class($provider));
        if ($requiresBoot) {
            $this->bootable[] = $provider;
        }
        $this->resolver->singleton($serviceProviderClass, new ValueBuilder($provider));
        foreach ($alias as $a) {
            $this->resolver->singleton($a, new ValueBuilder($provider));
        }
    }

    /**
     * Returns a closure that will build a provider on demand, if an implementation provided by the provider is
     * required.
     *
     * @param ServiceProvider $provider The provider instance to register.
     * @param string          $id       The id of the implementation to bind.
     *
     * @return Closure A Closure ready to be bound to the id as implementation.
     */
    private function getDeferredProviderMakeClosure(ServiceProvider $provider, $id)
    {
        return function () use ($provider, $id) {
            static $registered;
            if ($registered === null) {
                $provider->register();
                $registered = true;
            }

            return $this->get($id);
        };
    }

    /**
     * Binds an interface, a class or a string slug to an implementation.
     *
     * Existing implementations are replaced.
     *
     * @param string             $id                A class or interface fully qualified name or a string slug.
     * @param mixed              $implementation    The implementation that should be bound to the alias(es); can be a
     *                                              class name, an object or a closure.
     * @param array<string>|null $afterBuildMethods An array of methods that should be called on the built
     *                                              implementation after resolving it.
     *
     * @return void The method does not return any value.
     * @throws ContainerException      If there's an issue while trying to bind the implementation.
     */
    public function bind($id, $implementation = null, array $afterBuildMethods = null)
    {
        if ($implementation === null) {
            $implementation = $id;
        }
        if ($implementation === $id && !$this->classIsInstantiable($implementation)) {
            throw new NotFoundException("Class {$implementation} does not exist.");
        }
        $this->resolver->bind($id, $this->builders->getBuilder($id, $implementation, $afterBuildMethods));
    }

    /**
     * Boots up the application calling the `boot` method of each registered service provider.
     *
     * If there are bootable providers (providers overloading the `boot` method) then the `boot` method will be
     * called on each bootable provider.
     *
     * @return void This method does not return any value.
     *
     * @see ServiceProvider::boot()
     */
    public function boot()
    {
        if (!empty($this->bootable)) {
            foreach ($this->bootable as $provider) {
                /** @var ServiceProvider $provider */
                $provider->boot();
            }
        }
    }

    /**
     * Binds a class, interface or string slug to a chain of implementations decorating a base
     * object; the chain will be lazily resolved only on the first call.
     *
     * The base decorated object must be the last element of the array.
     *
     * @param string                        $id                The class, interface or slug the decorator chain should
     *                                                         be bound to.
     * @param array<string|object|callable> $decorators        An array of implementations that decorate an object.
     * @param array<string>|null            $afterBuildMethods An array of methods that should be called on the
     *                                                         instance after it has been built; the methods should not
     *                                                         require any argument.
     *
     * @return void This method does not return any value.
     * @throws ContainerException
     */
    public function singletonDecorators($id, $decorators, array $afterBuildMethods = null)
    {
        $this->resolver->singleton($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods));
    }

    /**
     * Builds and returns a closure that will start building the chain of decorators.
     *
     * @param array<string|object|callable> $decorators        A list of decorators.
     * @param string                        $id                The id to bind the decorator tail to.
     * @param array<string>|null            $afterBuildMethods A set of method to run on the built decorated instance
     *                                                         after it's built.
     * @return BuilderInterface The callable or Closure that will start building the decorator chain.
     *
     * @throws ContainerException If there's any issue while trying to register any decorator step.
     */
    private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMethods = null)
    {
        $decorator = array_pop($decorators);

        if ($decorator === null) {
            throw new ContainerException('The decorator chain cannot be empty.');
        }

        do {
            $previous = isset($builder) ? $builder : null;
            $builder = $this->builders->getBuilder($id, $decorator, $afterBuildMethods, $previous);
            $decorator = array_pop($decorators);
            $afterBuildMethods = [];
        } while ($decorator !== null);

        return $builder;
    }

    /**
     * Binds a class, interface or string slug to to a chain of implementations decorating a
     * base object.
     *
     * The base decorated object must be the last element of the array.
     *
     * @param string                        $id                The class, interface or slug the decorator chain should
     *                                                         be bound to.
     * @param array<string|object|callable> $decorators        An array of implementations that decorate an object.
     * @param array<string>|null            $afterBuildMethods An array of methods that should be called on the
     *                                                         instance after it has been built; the methods should not
     *                                                         require any argument.
     *
     * @return void This method does not return any value.
     * @throws ContainerException If there's any issue binding the decorators.
     */
    public function bindDecorators($id, array $decorators, array $afterBuildMethods = null)
    {
        $this->resolver->bind($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods));
    }

    /**
     * Unsets a binding or tag in the container.
     *
     * @param mixed $offset The offset to unset.
     *
     * @return void The method does not return any value.
     */
    #[ReturnTypeWillChange]
    public function offsetUnset($offset)
    {
        if (!is_string($offset)) {
            return;
        }
        $this->resolver->unbind($offset);
        unset($this->tags[$offset]);
    }

    /**
     * Starts the `when->needs->give` chain for a contextual binding.
     *
     * @param string $class The fully qualified name of the requesting class.
     *
     * Example:
     *
     *      // Any class requesting an implementation of `LoggerInterface` will receive this implementation ...
     *      $container->singleton('LoggerInterface', 'FilesystemLogger');
     *      // But if the requesting class is `Worker` return another implementation
     *      $container->when('Worker')
     *          ->needs('LoggerInterface)
     *          ->give('RemoteLogger);
     *
     * @return Container The container instance, to continue the when/needs/give chain.
     */
    public function when($class)
    {
        $this->whenClass = $class;

        return $this;
    }

    /**
     * Second step of the `when->needs->give` chain for a contextual binding.
     *
     * Example:
     *
     *      // Any class requesting an implementation of `LoggerInterface` will receive this implementation ...
     *      $container->singleton('LoggerInterface', 'FilesystemLogger');
     *      // But if the requesting class is `Worker` return another implementation.
     *      $container->when('Worker')
     *          ->needs('LoggerInterface)
     *          ->give('RemoteLogger);
     *
     * @param string $id The class or interface needed by the class.
     *
     * @return Container The container instance, to continue the when/needs/give chain.
     */
    public function needs($id)
    {
        $this->needsClass = $id;

        return $this;
    }

    /**
     * Third step of the `when->needs->give` chain for a contextual binding.
     *
     * Example:
     *
     *      // any class requesting an implementation of `LoggerInterface` will receive this implementation...
     *      $container->singleton('LoggerInterface', 'FilesystemLogger');
     *      // but if the requesting class is `Worker` return another implementation
     *      $container->when('Worker')
     *          ->needs('LoggerInterface)
     *          ->give('RemoteLogger);
     *
     * @param mixed $implementation The implementation specified
     *
     * @return void This method does not return any value.
     * @throws NotFoundException
     */
    public function give($implementation)
    {
        $id = "{$this->whenClass}::{$this->needsClass}";
        $builder = $this->builders->getBuilder($id, $implementation);
        $this->resolver->setWhenNeedsGive($this->whenClass, $this->needsClass, $builder);
        unset($this->whenClass, $this->needsClass);
    }

    /**
     * Returns a lambda function suitable to use as a callback; when called the function will build the implementation
     * bound to `$id` and return the value of a call to `$method` method with the call arguments.
     *
     * @param string|object $id               A fully-qualified class name, a bound slug or an object o call the
     *                                        callback on.
     * @param string        $method           The method that should be called on the resolved implementation with the
     *                                        specified array arguments.
     *
     * @return callable The callback function.
     *
     * @throws ContainerException If the id is not a bound implementation or valid class name.
     */
    public function callback($id, $method)
    {
        $callbackIdPrefix = is_object($id) ? spl_object_hash($id) : $id;

        if (!is_string($callbackIdPrefix)) {
            $typeOfId = gettype($id);
            throw new ContainerException(
                "Callbacks can only be built on ids, class names or objects; '{$typeOfId}' is neither."
            );
        }

        if (!is_string($method)) {
            throw new ContainerException("Callbacks second argument must be a string method name.");
        }

        $callbackId = $callbackIdPrefix . '::' . $method;

        if (isset($this->callbacks[$callbackId])) {
            return $this->callbacks[$callbackId];
        }

        $callbackClosure = function (...$args) use ($id, $method) {
            $instance = is_string($id) ?
                $this->resolver->resolve($id)
                : $this->builders->getBuilder($id)->build();
            return $instance->{$method}(...$args);
        };

        if (is_string($id) && ($this->resolver->isSingleton($id) || $this->isStaticMethod($id, $method))) {
            // If we can know immediately, without actually resolving the binding, then build and cache immediately.
            $this->callbacks[$callbackId] = $callbackClosure;
        }

        return $callbackClosure;
    }

    /**
     * Whether a method of an id, possibly not a class, is static or not.
     *
     * @param object|string $object A class name, instance or something that does not map to a class.
     * @param string        $method The method to check.
     *
     * @return bool Whether a method of an id or class is static or not.
     */
    protected function isStaticMethod($object, $method)
    {
        $key = is_string($object) ? $object . '::' . $method : get_class($object) . '::' . $method;

        if (!isset($this->isStaticMethodCache[$key])) {
            try {
                $this->isStaticMethodCache[$key] = (new ReflectionMethod($object, $method))->isStatic();
            } catch (ReflectionException $e) {
                return false;
            }
        }

        return $this->isStaticMethodCache[$key];
    }

    /**
     * Returns a callable object that will build an instance of the specified class using the
     * specified arguments when called.
     *
     * The callable will be a closure on PHP 5.3+ or a lambda function on PHP 5.2.
     *
     * @param string|mixed       $id                The fully qualified name of a class or an interface.
     * @param array<mixed>       $buildArgs         An array of arguments that should be used to build the instance;
     *                                              note that any argument will be resolved using the container itself
     *                                              and bindings will apply.
     * @param array<string>|null $afterBuildMethods An array of methods that should be called on the built
     *                                              implementation after resolving it.
     *
     * @return callable  A callable function that will return an instance of the specified class when
     *                   called.
     */
    public function instance($id, array $buildArgs = [], array $afterBuildMethods = null)
    {
        return function () use ($id, $afterBuildMethods, $buildArgs) {
            if (is_string($id)) {
                return $this->resolver->resolveWithArgs($id, $afterBuildMethods, ...$buildArgs);
            }

            return $this->builders->getBuilder($id, $id, $afterBuildMethods, ...$buildArgs)->build();
        };
    }

    /**
     * Protects a value to make sure it will not be resolved, if callable or if the name of an existing class.
     *
     * @param mixed $value The value to protect.
     *
     * @return ValueBuilder A protected value instance, its value set to the provided value.
     */
    public function protect($value)
    {
        return new ValueBuilder($value);
    }

    /**
     * Returns the Service Provider instance registered.
     *
     * @param string $providerId The Service Provider clas to return the instance for.
     *
     * @return ServiceProvider The service provider instance.
     *
     * @throws NotFoundException|ContainerException If the Service Provider class was never registered in the container
     *                                              or there's an issue retrieving it.
     */
    public function getProvider($providerId)
    {
        if (!$this->resolver->isBound($providerId)) {
            throw new NotFoundException("Service provider '{$providerId}' is not registered in the container.");
        }

        $provider = $this->get($providerId);

        if (! $provider instanceof ServiceProvider) {
            throw new NotFoundException("Bound implementation for '{$providerId}' is not Service Provider.");
        }

        return $provider;
    }

    /**
     * Returns whether a binding exists in the container or not.
     *
     * `isBound($id)` returning `true` means the a call to `bind($id, $implementaion)` or `singleton($id,
     * $implementation)` (or equivalent ArrayAccess methods) was explicitly made.
     *
     * @param string $id The id to check for bindings in the container.
     *
     * @return bool Whether an explicit binding for the id exists in the container or not.
     */
    public function isBound($id)
    {
        return is_string($id) && $this->resolver->isBound($id);
    }

    /**
     * Sets the mask for the throwables that should be caught and re-thrown as container exceptions.
     *
     * @param int $maskThrowables The mask for the throwables that should be caught and re-thrown as container
     *
     * @return void
     */
    public function setExceptionMask($maskThrowables)
    {
        $this->maskThrowables = (int)$maskThrowables;
    }
}