Skip to content
Open
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
48 changes: 48 additions & 0 deletions src/Phaseolies/DI/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,54 @@ public function extend(string $abstract, callable $extender): void
];
}

/**
* Run a callback with some bindings temporarily replaced, then put every one
* of them back exactly as it was, even when the callback throws
*
* @param array<string, mixed> $overrides
* @param callable(self): mixed $callback
* @return mixed
*/
public function using(array $overrides, callable $callback): mixed
{
$saved = [];

foreach (array_keys($overrides) as $abstract) {
$saved[$abstract] = [
'binding' => array_key_exists($abstract, $this->bindings) ? $this->bindings[$abstract] : null,
'hasBinding' => array_key_exists($abstract, $this->bindings),
'instance' => $this->instances[$abstract] ?? null,
'hasInstance' => array_key_exists($abstract, $this->instances),
];
}

try {
foreach ($overrides as $abstract => $concrete) {
unset($this->instances[$abstract]);

if ($concrete instanceof \Closure || (is_string($concrete) && class_exists($concrete))) {
$this->bind($abstract, $concrete, true);
} else {
$this->instance($abstract, $concrete);
}
}

return $callback($this);
} finally {
foreach ($saved as $abstract => $state) {
unset($this->bindings[$abstract], $this->instances[$abstract]);

if ($state['hasBinding']) {
$this->bindings[$abstract] = $state['binding'];
}

if ($state['hasInstance']) {
$this->instances[$abstract] = $state['instance'];
}
}
}
}

/**
* Alias a type to a different name
*
Expand Down
89 changes: 89 additions & 0 deletions tests/Application/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3021,6 +3021,95 @@ public function testMultipleImmutableServicesGuardedIndependently()
$this->assertTrue($mailerThrew);
}

public function testUsingOverridesForTheCallbackAndRestoresAfterwards()
{
$this->container->instance('mailer', 'real');

$inside = $this->container->using(['mailer' => 'fake'], fn(Container $c) => $c->make('mailer'));

$this->assertSame('fake', $inside);
$this->assertSame('real', $this->container->make('mailer'));
}

public function testUsingRemovesBindingsThatDidNotExistBefore()
{
$this->container->using(['scratch' => 'value'], function (Container $c) {
$this->assertTrue($c->hasInstance('scratch'));
});

$this->assertFalse($this->container->hasInstance('scratch'));
$this->assertArrayNotHasKey('scratch', $this->container->getBindings());
$this->assertArrayNotHasKey('scratch', $this->container->getInstances());
}

public function testUsingRestoresEvenWhenTheCallbackThrows()
{
$this->container->instance('mailer', 'real');

try {
$this->container->using(['mailer' => 'fake'], function () {
throw new RuntimeException('boom');
});
$this->fail('Expected the exception to propagate');
} catch (RuntimeException $e) {
$this->assertSame('boom', $e->getMessage());
}

$this->assertSame('real', $this->container->make('mailer'));
}

public function testUsingClosureIsAFactoryCalledOnceForTheScope()
{
$calls = 0;

$this->container->using(['token' => function () use (&$calls) {
$calls++;

return new \stdClass();
}], function (Container $c) {
$this->assertSame($c->make('token'), $c->make('token'));
});

$this->assertSame(1, $calls);
}

public function testUsingBuildsAClassNameOverride()
{
$this->container->bind(TestInterface::class, AnotherImplementation::class);

$this->container->using([TestInterface::class => ConcreteImplementation::class], function (Container $c) {
$this->assertInstanceOf(ConcreteImplementation::class, $c->make(TestInterface::class));
});

$this->assertInstanceOf(AnotherImplementation::class, $this->container->make(TestInterface::class));
}

public function testUsingRestoresTheSameSingletonInstanceThatWasResolvedBefore()
{
$this->container->singleton(Counter::class);
$before = $this->container->make(Counter::class);

$this->container->using([Counter::class => new Counter()], function (Container $c) use ($before) {
$this->assertNotSame($before, $c->make(Counter::class));
});

$this->assertSame($before, $this->container->make(Counter::class));
}

public function testUsingCanBeNested()
{
$this->container->instance('level', 'outer');

$result = $this->container->using(['level' => 'one'], function (Container $c) {
$c->using(['level' => 'two'], fn() => $this->assertSame('two', $c->make('level')));

return $c->make('level');
});

$this->assertSame('one', $result);
$this->assertSame('outer', $this->container->make('level'));
}

public function testMailerServiceReadsWorkAfterFreeze()
{
$mailer = $this->container->make(MockMailerService::class);
Expand Down
Loading