Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/Illuminate/Container/Attributes/Bind.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Illuminate\Container\Attributes;

use Attribute;
use BackedEnum;
use InvalidArgumentException;
use UnitEnum;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Bind
{
/**
* The concrete class to bind to.
*
* @var class-string
*/
public string $concrete;

/**
* The environments the binding should apply for.
*
* @var non-empty-array<int, string>
*/
public array $environments = [];

/**
* Create a new attribute instance.
*
* @param class-string $concrete
* @param non-empty-array<int, \BackedEnum|\UnitEnum|non-empty-string>|non-empty-string $environments
*
* @throws \InvalidArgumentException
*/
public function __construct(
string $concrete,
string|array $environments = ['*'],
) {
$environments = array_filter(is_array($environments) ? $environments : [$environments]);

if ($environments === []) {
throw new InvalidArgumentException('The environment property must be set and cannot be empty.');
}

$this->concrete = $concrete;

$this->environments = array_map(fn ($environment) => match (true) {
$environment instanceof BackedEnum => $environment->value,
$environment instanceof UnitEnum => $environment->name,
default => $environment,
}, $environments);
}
}
100 changes: 99 additions & 1 deletion src/Illuminate/Container/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use ArrayAccess;
use Closure;
use Exception;
use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\Scoped;
use Illuminate\Container\Attributes\Singleton;
use Illuminate\Contracts\Container\BindingResolutionException;
Expand Down Expand Up @@ -122,6 +123,13 @@ class Container implements ArrayAccess, ContainerContract
*/
public $contextualAttributes = [];

/**
* Whether an abstract class has already had its attributes checked for bindings.
*
* @var array<class-string, true>
*/
protected $checkedForAttributeBindings = [];

/**
* All of the registered rebound callbacks.
*
Expand Down Expand Up @@ -178,6 +186,13 @@ class Container implements ArrayAccess, ContainerContract
*/
protected $afterResolvingAttributeCallbacks = [];

/**
* The callback used to determine the container's environment.
*
* @var (callable(array<int, string>|string): bool|string)|null
*/
protected $environmentResolver = null;

/**
* Define a contextual binding.
*
Expand Down Expand Up @@ -961,7 +976,65 @@ protected function getConcrete($abstract)
return $this->bindings[$abstract]['concrete'];
}

return $abstract;
if ($this->environmentResolver === null ||
($this->checkedForAttributeBindings[$abstract] ?? false) || ! is_string($abstract)) {
return $abstract;
}

$attributes = [];

try {
$attributes = (new ReflectionClass($abstract))->getAttributes(Bind::class);
} catch (ReflectionException) {
}

$this->checkedForAttributeBindings[$abstract] = true;

if ($attributes === []) {
return $abstract;
}

return $this->getConcreteBindingFromAttributes($abstract, $attributes);
}

/**
* Get the concrete binding for an abstract from the Bind attribute.
*
* @param string $abstract
* @param array<int, \ReflectionAttribute<Bind>> $reflectedAttributes
* @return mixed
*/
protected function getConcreteBindingFromAttributes($abstract, $reflectedAttributes)
{
$concrete = $maybeConcrete = null;

foreach ($reflectedAttributes as $reflectedAttribute) {
$instance = $reflectedAttribute->newInstance();

if ($instance->environments === ['*']) {
$maybeConcrete = $instance->concrete;

continue;
}

if ($this->currentEnvironmentIs($instance->environments)) {
$concrete = $instance->concrete;

break;
}
}

if ($maybeConcrete !== null && $concrete === null) {
$concrete = $maybeConcrete;
}

if ($concrete === null) {
return $abstract;
}

$this->bind($abstract, $concrete);

return $this->bindings[$abstract]['concrete'];
}

/**
Expand Down Expand Up @@ -1621,6 +1694,30 @@ public function forgetScopedInstances()
}
}

/**
* Set the callback which determines the current container environment.
*
* @param (callable(array<int, string>|string): bool|string)|null $callback
* @return void
*/
public function resolveEnvironmentUsing(?callable $callback)
{
$this->environmentResolver = $callback;
}

/**
* Determine the environment for the container.
*
* @param array<int, string>|string $environments
* @return bool
*/
public function currentEnvironmentIs($environments)
{
return $this->environmentResolver === null
? false
: call_user_func($this->environmentResolver, $environments);
}

/**
* Flush the container of all bindings and resolved instances.
*
Expand All @@ -1634,6 +1731,7 @@ public function flush()
$this->instances = [];
$this->abstractAliases = [];
$this->scopedInstances = [];
$this->checkedForAttributeBindings = [];
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public function bootstrap(Application $app)
// the environment in a web context where an "--env" switch is not present.
$app->detectEnvironment(fn () => $config->get('app.env', 'production'));

$app->resolveEnvironmentUsing($app->environment(...));

date_default_timezone_set($config->get('app.timezone', 'UTC'));

mb_internal_encoding('UTF-8');
Expand Down
Loading