From c0e766401dac5fdc8d2694253b54658ccc8cb631 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 4 Jul 2024 14:15:28 +1200 Subject: [PATCH] FIX Ensure cache is shared between CLI and webserver --- _config/cache.yml | 1 - src/Control/Email/TransportFactory.php | 2 +- src/Core/Cache/AbstractCacheFactory.php | 100 +++++++++++ src/Core/Cache/ApcuCacheFactory.php | 61 ++++--- src/Core/Cache/CacheFactory.php | 7 +- src/Core/Cache/DefaultCacheFactory.php | 170 ++++++------------ src/Core/Cache/FilesystemCacheFactory.php | 3 +- src/Core/Cache/ManifestCacheFactory.php | 9 +- src/Core/Cache/MemcachedCacheFactory.php | 53 +++--- src/Core/Cache/RedisCacheFactory.php | 56 ++++++ src/Core/Injector/Factory.php | 2 +- src/Core/Injector/InjectionCreator.php | 2 +- .../Injector/InjectorTest/EmptyFactory.php | 2 +- 13 files changed, 281 insertions(+), 187 deletions(-) create mode 100644 src/Core/Cache/AbstractCacheFactory.php create mode 100644 src/Core/Cache/RedisCacheFactory.php diff --git a/_config/cache.yml b/_config/cache.yml index 9400a527038..5f69d6e4f74 100644 --- a/_config/cache.yml +++ b/_config/cache.yml @@ -7,7 +7,6 @@ SilverStripe\Core\Injector\Injector: constructor: args: directory: '`TEMP_PATH`' - version: null logger: '%$Psr\Log\LoggerInterface' Psr\SimpleCache\CacheInterface.cacheblock: factory: SilverStripe\Core\Cache\CacheFactory diff --git a/src/Control/Email/TransportFactory.php b/src/Control/Email/TransportFactory.php index c8c656c80bc..99ff56919c6 100644 --- a/src/Control/Email/TransportFactory.php +++ b/src/Control/Email/TransportFactory.php @@ -12,7 +12,7 @@ */ class TransportFactory implements Factory { - public function create($service, array $params = []) + public function create(string $service, array $params = []): object { $dsn = Environment::getEnv('MAILER_DSN') ?: $params['dsn']; $dispatcher = $params['dispatcher']; diff --git a/src/Core/Cache/AbstractCacheFactory.php b/src/Core/Cache/AbstractCacheFactory.php new file mode 100644 index 00000000000..10d2f79d5c3 --- /dev/null +++ b/src/Core/Cache/AbstractCacheFactory.php @@ -0,0 +1,100 @@ +logger = $logger; + } + + /** + * Creates an object with a PSR-16 interface, usually from a PSR-6 class name. + * + * Quick explanation of caching standards: + * - Symfony cache implements the PSR-6 standard + * - Symfony provides adapters which wrap a PSR-6 backend with a PSR-16 interface + * - Silverstripe uses the PSR-16 interface to interact with caches. It does not directly interact with the PSR-6 classes + * - Psr\SimpleCache\CacheInterface is the php interface of the PSR-16 standard. All concrete cache classes Silverstripe code interacts with should implement this interface + * + * Further reading: + * - https://symfony.com/doc/current/components/cache/psr6_psr16_adapters.html#using-a-psr-6-cache-object-as-a-psr-16-cache + * - https://github.com/php-fig/simple-cache + */ + protected function createCache( + string $class, + array $args, + bool $useInjector = true + ): CacheInterface { + $classIsPsr6 = is_a($class, CacheItemPoolInterface::class, true); + $classIsPsr16 = is_a($class, CacheInterface::class, true); + if (!$classIsPsr6 && !$classIsPsr16) { + throw new InvalidArgumentException("class $class must implement one of " . CacheItemPoolInterface::class . ' or ' . CacheInterface::class); + } + $cacheAdapter = $this->instantiateCache($class, $args, $useInjector); + $psr16Cache = $this->prepareCacheForUse($cacheAdapter, $useInjector); + return $psr16Cache; + } + + /** + * Prepare a cache adapter for use. + * This wraps a PSR6 adapter inside a PSR16 one. It also adds the loggers. + */ + protected function prepareCacheForUse( + CacheItemPoolInterface|CacheInterface $cacheAdapter, + bool $useInjector + ): CacheInterface { + $loggerAdded = false; + if ($cacheAdapter instanceof CacheItemPoolInterface) { + $loggerAdded = $this->addLogger($cacheAdapter, $loggerAdded); + // Wrap the PSR-6 class inside a class with a PSR-16 interface + $cacheAdapter = $this->instantiateCache(Psr16Cache::class, [$cacheAdapter], $useInjector); + } + if (!$loggerAdded) { + $this->addLogger($cacheAdapter, $loggerAdded); + } + return $cacheAdapter; + } + + /** + * Instantiates a cache adapter, either via the dependency injector or using the new keyword. + */ + protected function instantiateCache( + string $class, + array $args, + bool $useInjector + ): CacheItemPoolInterface|CacheInterface { + if ($useInjector) { + // Injector is used for in most instances to allow modification of the cache implementations + return Injector::inst()->createWithArgs($class, $args); + } + // ManifestCacheFactory cannot use Injector because config is not available at that point + return new $class(...$args); + } + + private function addLogger(CacheItemPoolInterface|CacheInterface $cache): bool + { + if ($this->logger && ($cache instanceof LoggerAwareInterface)) { + $cache->setLogger($this->logger); + return true; + } + return false; + } +} diff --git a/src/Core/Cache/ApcuCacheFactory.php b/src/Core/Cache/ApcuCacheFactory.php index 188224063b2..eff5ff3e689 100644 --- a/src/Core/Cache/ApcuCacheFactory.php +++ b/src/Core/Cache/ApcuCacheFactory.php @@ -2,39 +2,54 @@ namespace SilverStripe\Core\Cache; -use SilverStripe\Core\Injector\Injector; +use Psr\SimpleCache\CacheInterface; +use RuntimeException; +use SilverStripe\Control\Director; +use SilverStripe\Core\Environment; use Symfony\Component\Cache\Adapter\ApcuAdapter; -use Symfony\Component\Cache\Psr16Cache; -class ApcuCacheFactory implements CacheFactory +/** + * Factory to instantiate an ApcuAdapter for use in caching. + * + * Note that APCu cache may not be shared between your webserver and the CLI. + * Flushing the cache from your terminal may not flush the cache used by the webserver. + * See https://github.com/symfony/symfony/discussions/54066 + */ +class ApcuCacheFactory extends AbstractCacheFactory { - /** - * @var string - */ - protected $version; - - /** - * @param string $version - */ - public function __construct($version = null) - { - $this->version = $version; - } - /** * @inheritdoc */ - public function create($service, array $params = []) + public function create(string $service, array $params = []): CacheInterface { + if (!$this->isSupported()) { + throw new RuntimeException('APCu is not supported in the current environment. Cannot use APCu cache.'); + } + $namespace = isset($params['namespace']) ? $params['namespace'] . '_' . md5(BASE_PATH) : md5(BASE_PATH); $defaultLifetime = isset($params['defaultLifetime']) ? $params['defaultLifetime'] : 0; - $psr6Cache = Injector::inst()->createWithArgs(ApcuAdapter::class, [ - $namespace, - $defaultLifetime, - $this->version - ]); - return Injector::inst()->createWithArgs(Psr16Cache::class, [$psr6Cache]); + // $version is optional - defaults to null. + $version = isset($params['version']) ? $params['version'] : Environment::getEnv('SS_APCU_VERSION'); + $useInjector = isset($args['useInjector']) ? $args['useInjector'] : true; + + return $this->createCache( + ApcuAdapter::class, + [$namespace, $defaultLifetime, $version], + $useInjector + ); + } + + private function isSupported(): bool + { + static $isSupported = null; + if (null === $isSupported) { + // Need to check for CLI because Symfony won't: https://github.com/symfony/symfony/pull/25080 + $isSupported = Director::is_cli() + ? filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL) && ApcuAdapter::isSupported() + : ApcuAdapter::isSupported(); + } + return $isSupported; } } diff --git a/src/Core/Cache/CacheFactory.php b/src/Core/Cache/CacheFactory.php index 994ac948226..83efbb68766 100644 --- a/src/Core/Cache/CacheFactory.php +++ b/src/Core/Cache/CacheFactory.php @@ -7,14 +7,9 @@ interface CacheFactory extends InjectorFactory { - /** * Note: While the returned object is used as a singleton (by the originating Injector->get() call), * this cache object shouldn't be a singleton itself - it has varying constructor args for the same service name. - * - * @param string $service - * @param array $params - * @return CacheInterface */ - public function create($service, array $params = []); + public function create(string $service, array $params = []): CacheInterface; } diff --git a/src/Core/Cache/DefaultCacheFactory.php b/src/Core/Cache/DefaultCacheFactory.php index b82d6f08a2b..1a881cb9d14 100644 --- a/src/Core/Cache/DefaultCacheFactory.php +++ b/src/Core/Cache/DefaultCacheFactory.php @@ -2,38 +2,24 @@ namespace SilverStripe\Core\Cache; -use InvalidArgumentException; -use Psr\Log\LoggerAwareInterface; +use LogicException; use Psr\Log\LoggerInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\SimpleCache\CacheInterface; -use SilverStripe\Control\Director; +use SilverStripe\Core\Environment; use SilverStripe\Core\Injector\Injector; -use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\PhpFilesAdapter; -use Symfony\Component\Cache\Psr16Cache; /** - * Returns the most performant combination of caches available on the system: - * - `PhpFilesCache` (PHP 7 with opcache enabled) - * - `ApcuCache` (requires APC) with a `FilesystemCache` fallback (for larger cache volumes) - * - `FilesystemCache` if none of the above is available - * - * Modelled after `Symfony\Component\Cache\Adapter\AbstractAdapter::createSystemCache()` + * Creates the following cache adapters: + * - `PhpFilesAdapter` (falls back to `FilesystemAdapter` if `PhpFilesAdapter` isn't supported) + * - An optional in-memory cache such as Redis, Memcached, or APCu. */ -class DefaultCacheFactory implements CacheFactory +class DefaultCacheFactory extends AbstractCacheFactory { - /** - * @var string Absolute directory path - */ - protected $args = []; - - /** - * @var LoggerInterface - */ - protected $logger; + protected array $args = []; /** * @param array $args List of global options to merge with args during create() @@ -42,148 +28,92 @@ class DefaultCacheFactory implements CacheFactory public function __construct($args = [], LoggerInterface $logger = null) { $this->args = $args; - $this->logger = $logger; + parent::__construct($logger); } /** * @inheritdoc */ - public function create($service, array $args = []) + public function create(string $service, array $args = []): CacheInterface { // merge args with default $args = array_merge($this->args, $args); $namespace = isset($args['namespace']) ? $args['namespace'] : ''; - $defaultLifetime = isset($args['defaultLifetime']) ? $args['defaultLifetime'] : 0; + $defaultLifetime = (int) (isset($args['defaultLifetime']) ? $args['defaultLifetime'] : 0); $directory = isset($args['directory']) ? $args['directory'] : null; - $version = isset($args['version']) ? $args['version'] : null; $useInjector = isset($args['useInjector']) ? $args['useInjector'] : true; // In-memory caches are typically more resource constrained (number of items and storage space). // Give cache consumers an opt-out if they are expecting to create large caches with long lifetimes. $useInMemoryCache = isset($args['useInMemoryCache']) ? $args['useInMemoryCache'] : true; + $inMemoryCacheFactory = Environment::getEnv('SS_MEMORY_CACHEFACTORY'); - // Check support - $apcuSupported = ($this->isAPCUSupported() && $useInMemoryCache); - $phpFilesSupported = $this->isPHPFilesSupported(); - - // If apcu isn't supported, phpfiles is the next best preference - if (!$apcuSupported && $phpFilesSupported) { - return $this->createCache(PhpFilesAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector); + $filesystemCache = $this->instantiateFilesystemCache($namespace, $defaultLifetime, $directory, $useInjector); + if (!$useInMemoryCache || !$inMemoryCacheFactory) { + return $this->prepareCacheForUse($filesystemCache, $useInjector); } - // Create filesystem cache - if (!$apcuSupported) { - return $this->createCache( - FilesystemAdapter::class, - [$namespace, $defaultLifetime, $directory], - $useInjector + // Check if SS_MEMORY_CACHEFACTORY is a factory + if (!is_a($inMemoryCacheFactory, CacheFactory::class, true)) { + throw new LogicException( + 'SS_MEMORY_CACHEFACTORY is not a valid CacheFactory class name' ); } - // Create PSR6 filesystem + apcu cache's wrapped in a PSR6 chain adapter, then wrap in a PSR16 class - $fs = $this->instantiateCache( - FilesystemAdapter::class, - [$namespace, $defaultLifetime, $directory], + // Note that the cache lifetime will be shorter there by default, to ensure there's enough + // resources for "hot cache" items as a resource constrained in memory cache. + $inMemoryLifetime = (int) ($defaultLifetime / 5); + $inMemoryCache = $this->instantiateInMemoryCache( + $service, + $inMemoryCacheFactory, + ['namespace' => $namespace, 'defaultLifetime' => $inMemoryLifetime, 'useInjector' => $useInjector], $useInjector ); - // Note that the cache lifetime will be shorter there by default, to ensure there's enough - // resources for "hot cache" items in APCu as a resource constrained in memory cache. - $apcuNamespace = $namespace . ($namespace ? '_' : '') . md5(BASE_PATH); - $lifetime = (int) $defaultLifetime / 5; - $apcu = $this->instantiateCache(ApcuAdapter::class, [$apcuNamespace, $lifetime, $version], $useInjector); - - return $this->createCache(ChainAdapter::class, [[$apcu, $fs]], $useInjector); - } - - /** - * Determine if apcu is supported - * - * @return bool - */ - protected function isAPCUSupported() - { - static $apcuSupported = null; - if (null === $apcuSupported) { - // Need to check for CLI because Symfony won't: https://github.com/symfony/symfony/pull/25080 - $apcuSupported = Director::is_cli() - ? filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOL) && ApcuAdapter::isSupported() - : ApcuAdapter::isSupported(); - } - return $apcuSupported; + return $this->createCache(ChainAdapter::class, [[$inMemoryCache, $filesystemCache]], $useInjector); } /** * Determine if PHP files is supported - * - * @return bool */ - protected function isPHPFilesSupported() + protected function isPHPFilesSupported(): bool { static $phpFilesSupported = null; if (null === $phpFilesSupported) { - $phpFilesSupported = PhpFilesAdapter::isSupported(); + // Only consider to be enabled if opcache is enabled in CLI, or else + // filesystem cache won't be shared between webserver and CLI. + $phpFilesSupported = PhpFilesAdapter::isSupported() && + filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL); } return $phpFilesSupported; } /** - * Creates an object with a PSR-16 interface, usually from a PSR-6 class name - * - * Quick explanation of caching standards: - * - Symfony cache implements the PSR-6 standard - * - Symfony provides adapters which wrap a PSR-6 backend with a PSR-16 interface - * - Silverstripe uses the PSR-16 interface to interact with caches. It does not directly interact with the PSR-6 classes - * - Psr\SimpleCache\CacheInterface is the php interface of the PSR-16 standard. All concrete cache classes Silverstripe code interacts with should implement this interface - * - * Further reading: - * - https://symfony.com/doc/current/components/cache/psr6_psr16_adapters.html#using-a-psr-6-cache-object-as-a-psr-16-cache - * - https://github.com/php-fig/simple-cache + * Instantiate the cache adapter for the filesystem cache. */ - protected function createCache( - string $class, - array $args, - bool $useInjector = true - ): CacheInterface { - $loggerAdded = false; - $classIsPsr6 = is_a($class, CacheItemPoolInterface::class, true); - $classIsPsr16 = is_a($class, CacheInterface::class, true); - if (!$classIsPsr6 && !$classIsPsr16) { - throw new InvalidArgumentException("class $class must implement one of " . CacheItemPoolInterface::class . ' or ' . CacheInterface::class); - } - if ($classIsPsr6) { - $psr6Cache = $this->instantiateCache($class, $args, $useInjector); - $loggerAdded = $this->addLogger($psr6Cache, $loggerAdded); - // Wrap the PSR-6 class inside a class with a PSR-16 interface - $psr16Cache = $this->instantiateCache(Psr16Cache::class, [$psr6Cache], $useInjector); - } else { - $psr16Cache = $this->instantiateCache($class, $args, $useInjector); - } - if (!$loggerAdded) { - $this->addLogger($psr16Cache, $loggerAdded); - } - return $psr16Cache; - } - - private function instantiateCache( - string $class, - array $args, + private function instantiateFilesystemCache( + string $namespace, + int $defaultLifetime, + string $directory, bool $useInjector - ): CacheItemPoolInterface|CacheInterface { - if ($useInjector) { - // Injector is used for in most instances to allow modification of the cache implementations - return Injector::inst()->createWithArgs($class, $args); + ): CacheInterface|CacheItemPoolInterface { + if ($this->isPHPFilesSupported()) { + return $this->instantiateCache(PhpFilesAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector); } - // ManifestCacheFactory cannot use Injector because config is not available at that point - return new $class(...$args); + return $this->instantiateCache(FilesystemAdapter::class, [$namespace, $defaultLifetime, $directory], $useInjector); } - private function addLogger(CacheItemPoolInterface|CacheInterface $cache): bool + /** + * Instantiate the cache adapter for the in-memory cache. + */ + private function instantiateInMemoryCache(string $service, string $inMemoryCacheFactory, array $args): CacheInterface { - if ($this->logger && $cache instanceof LoggerAwareInterface) { - $cache->setLogger($this->logger); - return true; + if ($args['useInjector']) { + $factory = Injector::inst()->create($inMemoryCacheFactory); + } else { + $factory = new $inMemoryCacheFactory(); } - return false; + /** @var CacheFactory $factory */ + return $factory->create($service, $args); } } diff --git a/src/Core/Cache/FilesystemCacheFactory.php b/src/Core/Cache/FilesystemCacheFactory.php index 822f582f54d..4de5c7d3f36 100644 --- a/src/Core/Cache/FilesystemCacheFactory.php +++ b/src/Core/Cache/FilesystemCacheFactory.php @@ -2,6 +2,7 @@ namespace SilverStripe\Core\Cache; +use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\Injector\Injector; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Psr16Cache; @@ -24,7 +25,7 @@ public function __construct($directory) /** * @inheritdoc */ - public function create($service, array $params = []) + public function create(string $service, array $params = []): CacheInterface { $psr6Cache = Injector::inst()->createWithArgs(FilesystemAdapter::class, [ (isset($params['namespace'])) ? $params['namespace'] : '', diff --git a/src/Core/Cache/ManifestCacheFactory.php b/src/Core/Cache/ManifestCacheFactory.php index 52d3b294fe2..8c0b9b688f1 100644 --- a/src/Core/Cache/ManifestCacheFactory.php +++ b/src/Core/Cache/ManifestCacheFactory.php @@ -33,14 +33,9 @@ public function __construct(array $args = [], LoggerInterface $logger = null) } /** - * Note: While the returned object is used as a singleton (by the originating Injector->get() call), - * this cache object shouldn't be a singleton itself - it has varying constructor args for the same service name. - * - * @param string $service The class name of the service. - * @param array $params The constructor parameters. - * @return CacheInterface + * @inheritDoc */ - public function create($service, array $params = []) + public function create(string $service, array $params = []): CacheInterface { // Override default cache generation with SS_MANIFESTCACHE $cacheClass = Environment::getEnv('SS_MANIFESTCACHE'); diff --git a/src/Core/Cache/MemcachedCacheFactory.php b/src/Core/Cache/MemcachedCacheFactory.php index 1752baf5ba9..4c98556b0f7 100644 --- a/src/Core/Cache/MemcachedCacheFactory.php +++ b/src/Core/Cache/MemcachedCacheFactory.php @@ -2,41 +2,44 @@ namespace SilverStripe\Core\Cache; -use SilverStripe\Core\Injector\Injector; use Symfony\Component\Cache\Adapter\MemcachedAdapter; -use Symfony\Component\Cache\Psr16Cache; -use Memcached; +use Psr\SimpleCache\CacheInterface; +use RuntimeException; +use SilverStripe\Core\Environment; -class MemcachedCacheFactory implements CacheFactory +/** + * Factory to instantiate a MemcachedAdapter for use in caching. + * + * SS_MEMCACHED_DSN must be set in environment variables. + * See https://symfony.com/doc/current/components/cache/adapters/memcached_adapter.html#configure-the-connection + */ +class MemcachedCacheFactory extends AbstractCacheFactory { - - /** - * @var Memcached - */ - protected $memcachedClient; - - /** - * @param Memcached $memcachedClient - */ - public function __construct(Memcached $memcachedClient = null) - { - $this->memcachedClient = $memcachedClient; - } - /** * @inheritdoc */ - public function create($service, array $params = []) + public function create(string $service, array $params = []): CacheInterface { + if (!MemcachedAdapter::isSupported()) { + throw new RuntimeException('Memcached is not supported in the current environment. Cannot use Memcached cache.'); + } + + $dsn = Environment::getEnv('SS_MEMCACHED_DSN'); + if (!$dsn) { + throw new RuntimeException('The SS_MEMCACHED_DSN environment variable must be set to use Memcached cache.'); + } + $namespace = isset($params['namespace']) ? $params['namespace'] . '_' . md5(BASE_PATH) : md5(BASE_PATH); $defaultLifetime = isset($params['defaultLifetime']) ? $params['defaultLifetime'] : 0; - $psr6Cache = Injector::inst()->createWithArgs(MemcachedAdapter::class, [ - $this->memcachedClient, - $namespace, - $defaultLifetime - ]); - return Injector::inst()->createWithArgs(Psr16Cache::class, [$psr6Cache]); + $useInjector = isset($args['useInjector']) ? $args['useInjector'] : true; + $client = MemcachedAdapter::createConnection($dsn); + + return $this->createCache( + MemcachedAdapter::class, + [$client, $namespace, $defaultLifetime], + $useInjector + ); } } diff --git a/src/Core/Cache/RedisCacheFactory.php b/src/Core/Cache/RedisCacheFactory.php new file mode 100644 index 00000000000..9a789514e71 --- /dev/null +++ b/src/Core/Cache/RedisCacheFactory.php @@ -0,0 +1,56 @@ +isSupported()) { + throw new RuntimeException('Redis is not supported in the current environment. Cannot use Redis cache.'); + } + + $dsn = Environment::getEnv('SS_REDIS_DSN'); + if (!$dsn) { + throw new RuntimeException('The SS_REDIS_DSN environment variable must be set to use Redis cache.'); + } + + $namespace = isset($params['namespace']) + ? $params['namespace'] . '_' . md5(BASE_PATH) + : md5(BASE_PATH); + $defaultLifetime = isset($params['defaultLifetime']) ? $params['defaultLifetime'] : 0; + $useInjector = isset($args['useInjector']) ? $args['useInjector'] : true; + $client = RedisAdapter::createConnection($dsn, ['lazy' => true]); + + return $this->createCache( + RedisAdapter::class, + [$client, $namespace, $defaultLifetime], + $useInjector + ); + } + + public function isSupported() + { + return class_exists(PredisClient::class) || + class_exists(Relay::class) || + extension_loaded('redis'); + } +} diff --git a/src/Core/Injector/Factory.php b/src/Core/Injector/Factory.php index 36aaa00e199..c027c40d353 100644 --- a/src/Core/Injector/Factory.php +++ b/src/Core/Injector/Factory.php @@ -15,5 +15,5 @@ interface Factory * @param array $params The constructor parameters. * @return object The created service instances. */ - public function create($service, array $params = []); + public function create(string $service, array $params = []): ?object; } diff --git a/src/Core/Injector/InjectionCreator.php b/src/Core/Injector/InjectionCreator.php index ad91f11d281..663227667a4 100644 --- a/src/Core/Injector/InjectionCreator.php +++ b/src/Core/Injector/InjectionCreator.php @@ -17,7 +17,7 @@ class InjectionCreator implements Factory * * @param string|object $class - string: The FQCN of the class, object: A class instance */ - public function create($class, array $params = []) + public function create(string $class, array $params = []): object { if (is_object($class ?? '')) { $class = get_class($class); diff --git a/tests/php/Core/Injector/InjectorTest/EmptyFactory.php b/tests/php/Core/Injector/InjectorTest/EmptyFactory.php index e713f4689ac..62fb930a4b3 100644 --- a/tests/php/Core/Injector/InjectorTest/EmptyFactory.php +++ b/tests/php/Core/Injector/InjectorTest/EmptyFactory.php @@ -6,7 +6,7 @@ class EmptyFactory implements Factory { - public function create($service, array $params = []) + public function create(string $service, array $params = []): ?object { return null; }