Skip to content

Commit 57463c8

Browse files
authored
Merge pull request #349 from gacela-project/feat/getRequired
Add getRequired() for type-safe service resolution
2 parents d16050c + 30d2e89 commit 57463c8

File tree

6 files changed

+136
-0
lines changed

6 files changed

+136
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add contextual bindings via GacelaConfig::when()
66
- Add service aliases via GacelaConfig::addAlias()
77
- Add protected services via GacelaConfig::addProtected()
8+
- Add `Gacela::getRequired()` and `Locator::getRequired()` methods for type-safe service resolution that throws `ServiceNotFoundException` instead of returning null
89

910
## [1.12.0](https://github.com/gacela-project/gacela/compare/1.11.0...1.12.0) - 2025-11-09
1011

src/Framework/Container/Locator.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Gacela\Framework\Container;
66

77
use Gacela\Framework\ClassResolver\GlobalInstance\AnonymousGlobal;
8+
use Gacela\Framework\Exception\ServiceNotFoundException;
89

910
/**
1011
* @internal
@@ -49,6 +50,23 @@ public static function getSingleton(string $className, ?Container $container = n
4950
return self::getInstance($container)->get($className);
5051
}
5152

53+
/**
54+
* Get a singleton from the container, throwing an exception if not found.
55+
* Use this when you expect the service to exist and want type-safe returns.
56+
*
57+
* @template T of object
58+
*
59+
* @param class-string<T> $className
60+
*
61+
* @throws ServiceNotFoundException
62+
*
63+
* @return T
64+
*/
65+
public static function getRequiredSingleton(string $className, ?Container $container = null): object
66+
{
67+
return self::getInstance($container)->getRequired($className);
68+
}
69+
5270
public static function getInstance(?Container $container = null): self
5371
{
5472
if (!self::$instance instanceof self) {
@@ -83,6 +101,26 @@ public function get(string $className)
83101
return $locatedInstance;
84102
}
85103

104+
/**
105+
* @template T of object
106+
*
107+
* @param class-string<T> $className
108+
*
109+
* @throws ServiceNotFoundException
110+
*
111+
* @return T
112+
*/
113+
public function getRequired(string $className): object
114+
{
115+
$instance = $this->get($className);
116+
117+
if ($instance === null) {
118+
throw new ServiceNotFoundException($className);
119+
}
120+
121+
return $instance;
122+
}
123+
86124
/**
87125
* @template T
88126
*

src/Framework/Container/LocatorInterface.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Gacela\Framework\Container;
66

7+
use Gacela\Framework\Exception\ServiceNotFoundException;
8+
79
interface LocatorInterface
810
{
911
/**
@@ -14,4 +16,18 @@ interface LocatorInterface
1416
* @return T|null
1517
*/
1618
public function get(string $className);
19+
20+
/**
21+
* Get a service from the container, throwing an exception if not found.
22+
* Use this when you expect the service to exist and want type-safe returns.
23+
*
24+
* @template T of object
25+
*
26+
* @param class-string<T> $className
27+
*
28+
* @throws ServiceNotFoundException
29+
*
30+
* @return T
31+
*/
32+
public function getRequired(string $className): object;
1733
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Gacela\Framework\Exception;
6+
7+
use RuntimeException;
8+
9+
use function sprintf;
10+
11+
final class ServiceNotFoundException extends RuntimeException
12+
{
13+
public function __construct(string $className)
14+
{
15+
parent::__construct(sprintf('Service "%s" not found in the container.', $className));
16+
}
17+
}

src/Framework/Gacela.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Gacela\Framework\Container\Container;
1919
use Gacela\Framework\Container\Locator;
2020
use Gacela\Framework\Exception\GacelaNotBootstrappedException;
21+
use Gacela\Framework\Exception\ServiceNotFoundException;
2122
use Gacela\Framework\ServiceResolver\DocBlockResolverCache;
2223

2324
use function is_string;
@@ -66,6 +67,23 @@ public static function get(string $className): mixed
6667
return Locator::getSingleton($className, self::$mainContainer);
6768
}
6869

70+
/**
71+
* Get a service from the container, throwing an exception if not found.
72+
* Use this when you expect the service to exist and want type-safe returns.
73+
*
74+
* @template T of object
75+
*
76+
* @param class-string<T> $className
77+
*
78+
* @throws ServiceNotFoundException
79+
*
80+
* @return T
81+
*/
82+
public static function getRequired(string $className): object
83+
{
84+
return Locator::getRequiredSingleton($className, self::$mainContainer);
85+
}
86+
6987
/**
7088
* Get the main dependency injection container.
7189
* This is the actual container created during bootstrap with all runtime bindings and frozen services.

tests/Unit/Framework/Container/LocatorTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Gacela\Framework\Container\Container;
88
use Gacela\Framework\Container\Locator;
9+
use Gacela\Framework\Exception\ServiceNotFoundException;
910
use GacelaTest\Fixtures\StringValue;
1011
use PHPUnit\Framework\TestCase;
1112

@@ -55,4 +56,49 @@ public function test_get_existing_singleton_from_container(): void
5556

5657
self::assertEquals(new StringValue('str'), $singleton);
5758
}
59+
60+
public function test_get_required_throws_when_not_found(): void
61+
{
62+
$this->expectException(ServiceNotFoundException::class);
63+
$this->expectExceptionMessage('Service "GacelaTest\Unit\Framework\Container\NonExisting" not found in the container.');
64+
65+
Locator::getInstance()->getRequired(NonExisting::class);
66+
}
67+
68+
public function test_get_required_singleton_throws_when_not_found(): void
69+
{
70+
$this->expectException(ServiceNotFoundException::class);
71+
$this->expectExceptionMessage('Service "GacelaTest\Unit\Framework\Container\NonExisting" not found in the container.');
72+
73+
Locator::getRequiredSingleton(NonExisting::class);
74+
}
75+
76+
public function test_get_required_returns_existing_service(): void
77+
{
78+
Locator::addSingleton(StringValue::class, new StringValue('required'));
79+
80+
$singleton = Locator::getInstance()->getRequired(StringValue::class);
81+
82+
self::assertEquals(new StringValue('required'), $singleton);
83+
}
84+
85+
public function test_get_required_singleton_returns_existing_service(): void
86+
{
87+
Locator::addSingleton(StringValue::class, new StringValue('required'));
88+
89+
$singleton = Locator::getRequiredSingleton(StringValue::class);
90+
91+
self::assertEquals(new StringValue('required'), $singleton);
92+
}
93+
94+
public function test_get_required_singleton_from_container(): void
95+
{
96+
$container = new Container(bindings: [
97+
StringValue::class => new StringValue('from-container'),
98+
]);
99+
100+
$singleton = Locator::getRequiredSingleton(StringValue::class, $container);
101+
102+
self::assertEquals(new StringValue('from-container'), $singleton);
103+
}
58104
}

0 commit comments

Comments
 (0)