Skip to content

Commit

Permalink
feat: Services lazy loading (#99)
Browse files Browse the repository at this point in the history
Closes #95

Services are instantiated only when get is called. It does not detect
circular dependency: if there is one it will go into infinite loop. It
the user role to detect it and fix the problem
  • Loading branch information
Gashmob authored Dec 23, 2024
2 parents fbfec61 + 41b2800 commit f1942bb
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 178 deletions.
6 changes: 2 additions & 4 deletions include/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function __construct(
private ServicesLoader $services_loader,
private EventsLoader $events_loader,
) {
$this->service_manager = new ServiceManager();
$this->service_manager = new ServiceManager((new MapperBuilder())->enableFlexibleCasting()->allowSuperfluousKeys()->allowPermissiveTypes()->mapper());
$this->event_manager = new EventManager($this->service_manager);
$environment = new Environment();
$this->service_manager->add($this->event_manager);
Expand All @@ -78,9 +78,7 @@ public static function build(): self
{
return new self(
new LoadBricks(),
new LoadServices(
(new MapperBuilder())->enableFlexibleCasting()->allowSuperfluousKeys()->allowPermissiveTypes()->mapper()
),
new LoadServices(),
new LoadEvents(),
);
}
Expand Down
144 changes: 2 additions & 142 deletions include/Services/LoadServices.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,155 +27,15 @@

namespace Archict\Core\Services;

use Archict\Core\Env\EnvironmentService;
use Composer\InstalledVersions;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\TreeMapper;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use Symfony\Component\Yaml\Yaml;
use function Psl\File\read;
use function Psl\Filesystem\exists;

/**
* @internal
*/
final readonly class LoadServices implements ServicesLoader
{
public function __construct(
private TreeMapper $mapper,
) {
}

/**
* @throws ServicesCannotBeLoadedException
* @throws ServiceConfigurationFileFormatInvalidException
* @throws ServiceConfigurationFileNotFoundException
*/
public function loadServicesIntoManager(ServiceManager $manager, array $services_representation): void
{
$has_changed = true;
$services_to_init = $services_representation;
while ($has_changed && !empty($services_to_init)) {
$has_changed = false;
$leftovers = [];

foreach ($services_to_init as $service) {
$instance = $this->instantiateService($service, $manager);
if ($instance === null) {
$leftovers[] = $service;
} else {
$manager->add($instance);
$has_changed = true;
}
}

$services_to_init = $leftovers;
}

if (!empty($services_to_init)) {
throw new ServicesCannotBeLoadedException(
array_map(
static fn(ServiceRepresentation $representation) => $representation->reflection->name,
$services_to_init
)
);
}
}

/**
* @throws ServiceConfigurationFileNotFoundException
* @throws ServiceConfigurationFileFormatInvalidException
*/
private function instantiateService(ServiceRepresentation $representation, ServiceManager $manager): ?object
{
$reflection = $representation->reflection;
$constructor = $reflection->getConstructor();
if ($constructor === null) {
try {
return $reflection->newInstance();
} catch (ReflectionException) {
return null;
}
}

$parameters = $constructor->getParameters();
$args = [];
foreach ($parameters as $parameter) {
$arg = $this->getParameter($parameter, $manager, $representation);
if ($arg === null) {
return null;
}

$args[] = $arg;
}

try {
return $reflection->newInstanceArgs($args);
} catch (ReflectionException) {
return null;
}
}

/**
* @throws ServiceConfigurationFileNotFoundException
* @throws ServiceConfigurationFileFormatInvalidException
*/
private function getParameter(ReflectionParameter $parameter, ServiceManager $manager, ServiceRepresentation $service): mixed
{
$type = $parameter->getType();
if (!($type instanceof ReflectionNamedType) || $type->isBuiltin()) {
return null;
}

$type_name = $type->getName(); // Assert it's a class-string
if ($manager->has($type_name)) { // @phpstan-ignore-line
return $manager->get($type_name); // @phpstan-ignore-line
}

if ($service->service_attribute->configuration_classname === $type_name) {
try {
return $this->mapper->map(
$type_name,
Yaml::parse(read($this->getConfigurationFilenameForService($service, $manager->get(EnvironmentService::class))))
);
} catch (MappingError $error) {
throw new ServiceConfigurationFileFormatInvalidException($service->reflection->getName(), $error);
}
}

return null;
}

/**
* @return non-empty-string
* @throws ServiceConfigurationFileNotFoundException
*/
private function getConfigurationFilenameForService(ServiceRepresentation $representation, ?EnvironmentService $env): string
{
$base_filename = $representation->service_attribute->configuration_filename ?? (strtolower($representation->reflection->getShortName()) . '.yml');

$package_config = $representation->package_path . '/config/' . $base_filename;
if ($env !== null) {
$config_dir = (string) $env->get('CONFIG_DIR', InstalledVersions::getRootPackage()['install_path'] . '/config/');
if ($config_dir !== '' && $config_dir[0] !== '/') {
$config_dir = InstalledVersions::getRootPackage()['install_path'] . '/' . $config_dir;
}
} else {
$config_dir = InstalledVersions::getRootPackage()['install_path'] . '/config/';
foreach ($services_representation as $service) {
$manager->addRepresentation($service);
}

$root_config = $config_dir . $base_filename;

if ($root_config !== '' && exists($root_config)) {
return $root_config;
}

if (exists($package_config)) {
return $package_config;
}

throw new ServiceConfigurationFileNotFoundException($representation->reflection->getName());
}
}
123 changes: 110 additions & 13 deletions include/Services/ServiceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,47 @@

namespace Archict\Core\Services;

use Archict\Core\Env\EnvironmentService;
use Composer\InstalledVersions;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\TreeMapper;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use Symfony\Component\Yaml\Yaml;
use function Psl\File\read;
use function Psl\Filesystem\exists;

/**
* ServiceManager is a Service
*/
final class ServiceManager
{
/**
* @var array<class-string, object>
* @var array<class-string, array{
* representation: ServiceRepresentation,
* instance: ?object,
* }>
*/
private array $services = [];

public function __construct()
public function __construct(private readonly TreeMapper $mapper)
{
$this->add($this);
}

/**
* @internal
*/
public function addRepresentation(ServiceRepresentation $service): void
{
$this->services[$service->reflection->getName()] = [
'representation' => $service,
'instance' => null,
];
}

/**
* @param class-string $classname
*/
Expand All @@ -64,13 +86,19 @@ public function has(string $classname): bool
* @psalm-template S of object
* @psalm-param class-string<S> $classname
* @psalm-return ?S
* @throws ServiceConfigurationFileFormatInvalidException
* @throws ServiceConfigurationFileNotFoundException
* @psalm-suppress InvalidReturnType,InvalidReturnStatement
*/
public function get(string $classname): ?object
{
foreach ($this->services as $service_class => $service) {
if ($this->isServiceMatchClassName($service_class, $classname)) {
return $service; // @phpstan-ignore-line
if ($service['instance'] === null) {
$this->services[$service_class]['instance'] = $this->instantiateWithServicesIntern($service_class, $service['representation']);
}

return $this->services[$service_class]['instance']; // @phpstan-ignore return.type
}
}

Expand All @@ -79,16 +107,29 @@ public function get(string $classname): ?object

public function add(object $service): void
{
$this->services[$service::class] = $service;
$this->services[$service::class]['instance'] = $service;
}

/**
* @psalm-template C of object
* @param class-string<C> $class
* @return ?C
* @psalm-suppress InvalidReturnType,InvalidReturnStatement
* @throws ServiceConfigurationFileFormatInvalidException
* @throws ServiceConfigurationFileNotFoundException
*/
public function instantiateWithServices(string $class): ?object
{
return $this->instantiateWithServicesIntern($class, null);
}

/**
* @psalm-template C of object
* @param class-string<C> $class
* @return ?C
* @throws ServiceConfigurationFileFormatInvalidException
* @throws ServiceConfigurationFileNotFoundException
*/
private function instantiateWithServicesIntern(string $class, ?ServiceRepresentation $service): ?object
{
if (!class_exists($class)) {
return null;
Expand All @@ -107,17 +148,12 @@ public function instantiateWithServices(string $class): ?object
$parameters = $constructor->getParameters();
$args = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if (!($type instanceof ReflectionNamedType) || $type->isBuiltin()) {
$arg = $this->getServiceParameter($parameter, $this, $service);
if ($arg === null) {
return null;
}

$type_name = $type->getName(); // Assert it's a class-string
if ($this->has($type_name)) { // @phpstan-ignore-line
$args[] = $this->get($type_name); // @phpstan-ignore-line
} else {
return null;
}
$args[] = $arg;
}

try {
Expand All @@ -127,6 +163,67 @@ public function instantiateWithServices(string $class): ?object
}
}

/**
* @throws ServiceConfigurationFileNotFoundException
* @throws ServiceConfigurationFileFormatInvalidException
*/
private function getServiceParameter(ReflectionParameter $parameter, ServiceManager $manager, ?ServiceRepresentation $service): mixed
{
$type = $parameter->getType();
if (!($type instanceof ReflectionNamedType) || $type->isBuiltin()) {
return null;
}

$type_name = $type->getName(); // Assert it's a class-string
if ($manager->has($type_name)) { // @phpstan-ignore-line
return $manager->get($type_name); // @phpstan-ignore-line
}

if ($service !== null && $service->service_attribute->configuration_classname === $type_name) {
try {
return $this->mapper->map(
$type_name,
Yaml::parse(read($this->getConfigurationFilenameForService($service, $manager->get(EnvironmentService::class)))),
);
} catch (MappingError $error) {
throw new ServiceConfigurationFileFormatInvalidException($service->reflection->getName(), $error);
}
}

return null;
}

/**
* @return non-empty-string
* @throws ServiceConfigurationFileNotFoundException
*/
private function getConfigurationFilenameForService(ServiceRepresentation $representation, ?EnvironmentService $env): string
{
$base_filename = $representation->service_attribute->configuration_filename ?? (strtolower($representation->reflection->getShortName()) . '.yml');

$package_config = $representation->package_path . '/config/' . $base_filename;
if ($env !== null) {
$config_dir = (string) $env->get('CONFIG_DIR', InstalledVersions::getRootPackage()['install_path'] . '/config/');
if ($config_dir !== '' && $config_dir[0] !== '/') {
$config_dir = InstalledVersions::getRootPackage()['install_path'] . '/' . $config_dir;
}
} else {
$config_dir = InstalledVersions::getRootPackage()['install_path'] . '/config/';
}

$root_config = $config_dir . $base_filename;

if ($root_config !== '' && exists($root_config)) {
return $root_config;
}

if (exists($package_config)) {
return $package_config;
}

throw new ServiceConfigurationFileNotFoundException($representation->reflection->getName());
}

/**
* @param class-string $service
* @param class-string $classname
Expand Down
Loading

0 comments on commit f1942bb

Please sign in to comment.