Skip to content
Closed
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
81 changes: 81 additions & 0 deletions Tests/ContainerResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,87 @@ public function testGetInstanceWithInstanceInNonSharedMode()
$this->assertNotSame($stub, $resource->getInstance());
}

/**
* @testdox If resource is lazy, a lazy proxy object is returned
*
* @covers Joomla\DI\Container
* @uses Joomla\DI\ContainerResource
*/
public function testGetInstanceInLazyMode()
{
if (PHP_VERSION_ID < 80400) {
$this->markTestSkipped('Lazy objects are only supported in PHP 8.4 or newer.');
}

$container = new Container();
$container->set('stub1', fn() => new Stub1(), true, true);

$factoryCalled = false;

$resource = new ContainerResource(
$container,
static function($container) use (&$factoryCalled) {
$factoryCalled = true;
return new Stub2($container->get('stub1'));
},
ContainerResource::LAZY,
Stub2::class
);

$stub2 = $resource->getInstance(false);

$this->assertTrue(
(new \ReflectionClass(Stub2::class))->isUninitializedLazyObject($stub2),
'Lazy proxy object should be returned'
);
$this->assertFalse(
$factoryCalled,
'Factory should not be called before object state is observed or modified'
);
$this->assertSame(
$container->get('stub1'),
$stub2->stub,
'Factory should be called when object state is observed or modified'
);
}

/**
* @testdox If resource is lazy, but lazy objects are not supported, a normal object is returned
*
* @covers Joomla\DI\Container
* @uses Joomla\DI\ContainerResource
*/
public function testGetInstanceInLazyModeNotSupported()
{
if (PHP_VERSION_ID >= 80400) {
$this->markTestSkipped();
}

$container = new Container();
$container->set('stub1', fn() => new Stub1(), true, true);

$resource = new ContainerResource(
$container,
static function($container) {
return new Stub2($container->get('stub1'));
},
ContainerResource::LAZY,
Stub2::class
);

$stub2 = $resource->getInstance(false);

ob_start();
var_dump($stub2);
$type = ob_get_clean();

$this->assertStringStartsWith(
'object(' . Stub2::class,
$type,
'Normal object should be returned'
);
}

/**
* @testdox After a reset, a new instance is returned even for shared resources with factories
*
Expand Down
40 changes: 40 additions & 0 deletions Tests/ContainerSetupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,46 @@ static function () {
$this->assertTrue($container->isProtected('foo'));
}

/**
* @testdox The convenience method lazy() sets resources as lazy, but not protected and shared by default
*
* @covers Joomla\DI\Container
* @uses Joomla\DI\ContainerResource
*/
public function testLazy()
{
$container = new Container();
$container->lazy(
\stdClass::class,
static function () {
return new \stdClass();
},
);

$this->assertFalse($container->isShared(\stdClass::class));
$this->assertFalse($container->isProtected(\stdClass::class));
}

/**
* @testdox The convenience method lazy() throws an exception when lazy class does not exist
*
* @covers Joomla\DI\Container
* @uses Joomla\DI\ContainerResource
*/
public function testLazyClassNotExists()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Lazy key must be a valid class name: "ThisClassDoesNotExist"');

$container = new Container();
$container->lazy(
'ThisClassDoesNotExist',
static function () {
return new \stdClass();
},
);
}

/**
* @testdox The callback gets the container instance as a parameter
*
Expand Down
57 changes: 57 additions & 0 deletions Tests/ResourceDecorationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,63 @@ static function ($shared) {
);
}

/**
* @testdox A lazy resource can be extended
*
* @covers Joomla\DI\Container
* @uses Joomla\DI\ContainerResource
*/
public function testExtendLazy()
{
if (PHP_VERSION_ID < 80400) {
$this->markTestSkipped('Lazy objects are only supported in PHP 8.4 or newer.');
}

$factoryCalled = false;
$extendCalled = false;

$container = new Container();
$container->lazy(
Stub2::class,
static function () use (&$factoryCalled) {
$factoryCalled = true;

return new Stub2(new Stub1());
}
);

$container->extend(
Stub2::class,
static function ($lazy) use (&$extendCalled) {
$extendCalled = true;

$lazy->stub = 'stub1';

return $lazy;
}
);

$stub2 = $container->get(Stub2::class);

$this->assertTrue(
(new \ReflectionClass(Stub2::class))->isUninitializedLazyObject($stub2),
'Lazy proxy object should be returned'
);
$this->assertFalse(
$factoryCalled,
'Factory should not be called before object state is observed or modified'
);
$this->assertFalse(
$extendCalled,
'Extend callable should not be called before object state is observed or modified'
);
$this->assertSame(
'stub1',
$stub2->stub,
'Extend callable should be called after the factory'
);
}

/**
* A base method defining a resource in a container
*
Expand Down
41 changes: 38 additions & 3 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function get($resourceName)
throw new KeyNotFoundException(sprintf("Resource '%s' has not been registered with the container.", $resourceName));
}

return $this->resources[$key]->getInstance();
return $this->resources[$key]->getInstance(false);
}

/**
Expand Down Expand Up @@ -435,10 +435,14 @@ public function extend($resourceName, callable $callable)
$resource = $this->getResource($key, true);

$closure = function ($c) use ($callable, $resource) {
return $callable($resource->getInstance(), $c);
return $callable($resource->getInstance(true), $c);
};

$this->set($key, $closure, $resource->isShared());
if ($resource->isLazy()) {
$this->lazy($key, $closure, $resource->isShared(), false);
} else {
$this->set($key, $closure, $resource->isShared(), false);
}
}

/**
Expand Down Expand Up @@ -665,6 +669,37 @@ public function share($key, $value, $protected = false)
return $this->set($key, $value, true, $protected);
}

/**
* Shortcut method for creating lazy keys.
*
* @param string $key Name of dataStore key to set.
* @param mixed $value Callable function to run or string to retrieve when requesting the specified $key.
* @param boolean $shared True to create and store a shared instance.
* @param boolean $protected True to protect this item from being overwritten. Useful for services.
*
* @return $this
*
* @since __DEPLOY_VERSION__
*/
public function lazy($key, $value, $shared = false, $protected = false): self
{
if ($this->has($key) && $this->isLocal($key) && $this->isProtected($key)) {
throw new ProtectedKeyException(sprintf("Key %s is protected and can't be overwritten.", $key));
}

if (!class_exists($key)) {
throw new \InvalidArgumentException(sprintf('Lazy key must be a valid class name: "%s".', $key));
}

$mode = ContainerResource::LAZY;
$mode |= $shared ? ContainerResource::SHARE : ContainerResource::NO_SHARE;
$mode |= $protected ? ContainerResource::PROTECT : ContainerResource::NO_PROTECT;

$this->resources[$key] = new ContainerResource($this, $value, $mode, $key);

return $this;
}

/**
* Get the raw data assigned to a key.
*
Expand Down
Loading