diff --git a/app/AppKernel.php b/app/AppKernel.php deleted file mode 100644 index ad8900b..0000000 --- a/app/AppKernel.php +++ /dev/null @@ -1,35 +0,0 @@ -load(__DIR__ . '/config.yml'); - } - - public function __construct($environment, $debug) - { - parent::__construct($environment, $debug); - - $loader = require __DIR__.'/../vendor/autoload.php'; - - AnnotationRegistry::registerLoader(array($loader, 'loadClass')); - } -} diff --git a/app/config.yml b/app/config.yml index 0cfd648..eb7af08 100644 --- a/app/config.yml +++ b/app/config.yml @@ -1,10 +1,6 @@ parameters: cache.service: cache.service -jms_aop: - cache_dir: %kernel.cache_dir%/jms_aop - - monolog: handlers: main: @@ -12,6 +8,9 @@ monolog: level: info services: + cache_warmer: + class: Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate + arguments: [""] cache.service: class: Symfony\Component\Cache\Adapter\ArrayAdapter cache.testservice: diff --git a/composer.json b/composer.json index cc24841..ca9021a 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,8 @@ } ], "require": { - "php": ">=5.6", - "jms/aop-bundle": "1.*", + "php": ">=7.0", + "ocramius/proxy-manager": "@stable", "psr/log": "~1", "psr/cache": "1.0.*", "doctrine/annotations": "@stable" @@ -31,8 +31,7 @@ "symfony/framework-bundle": "@stable" }, "autoload": { - "psr-4": { "CacheBundle\\": "src/CacheBundle" }, - "psr-0": { "CacheBundle\\Annotation": "src/" } + "psr-4": { "CacheBundle\\": "src/CacheBundle/" } }, "config": { "optimize-autoloader": true diff --git a/src/CacheBundle/CacheBundle.php b/src/CacheBundle/CacheBundle.php index 55aed65..57a69de 100644 --- a/src/CacheBundle/CacheBundle.php +++ b/src/CacheBundle/CacheBundle.php @@ -2,16 +2,33 @@ namespace CacheBundle; -use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use CacheBundle\DependencyInjection\CacheCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Bundle\Bundle; class CacheBundle extends Bundle { + protected $autoloader; + /** @var \ProxyManager\Configuration */ + protected $config; + protected $getProxyConfig; + + public function shutdown() + { + spl_autoload_unregister($this->container->get('emag.cache.proxy.config')->getProxyAutoloader()); + } + + public function boot() + { + $this->autoloader = spl_autoload_register($this->container->get('emag.cache.proxy.config')->getProxyAutoloader()); + } + public function build(ContainerBuilder $container) { - parent::build($container); + $compilerPass = new CacheCompilerPass(); - $container->addCompilerPass(new CacheCompilerPass(), PassConfig::TYPE_OPTIMIZE); + $container->addCompilerPass($compilerPass); + parent::build($container); } } diff --git a/src/CacheBundle/CacheCompilerPass.php b/src/CacheBundle/CacheCompilerPass.php deleted file mode 100644 index f3cbfc9..0000000 --- a/src/CacheBundle/CacheCompilerPass.php +++ /dev/null @@ -1,49 +0,0 @@ -getParameter('cache.service')), - new Reference('annotation_reader'), - ] - ); - $interceptor->addMethodCall( - 'setLogger', - [ - new Reference('logger'), - ] - ); - $interceptor->addTag('monolog.logger', ['channel' => 'cache']); - $container->setDefinition('cache.interceptor', $interceptor); - - - $pointCut = new Definition( - 'CacheBundle\DependencyInjection\PointCut', [ - new Reference('annotation_reader'), - ] - ); - $pointCut->addTag('jms_aop.pointcut', ['interceptor' => 'cache.interceptor']); - $container->setDefinition('cache.pointcut', $pointCut); - } - -} \ No newline at end of file diff --git a/src/CacheBundle/ContextAwareCache.php b/src/CacheBundle/ContextAwareCache.php deleted file mode 100644 index 5f334ca..0000000 --- a/src/CacheBundle/ContextAwareCache.php +++ /dev/null @@ -1,10 +0,0 @@ -mkdir( + str_replace( + '%kernel.cache_dir%', + $container->getParameter('kernel.cache_dir'), + $container->getParameter('emag.cacheable.service.path') + ) + ); + + + $this->proxyServicesToBeCached($container); + } + + /** + * @param ContainerBuilder $container + * @return array + */ + protected function proxyServicesToBeCached(ContainerBuilder $container) + { + $annotationReader = new AnnotationReader(); + $servicesToBeCached = []; + foreach ($container->getDefinitions() as $serviceId => $definition) { + if ($definition->getClass() === null) { + continue; + } + $originalReflection = new \ReflectionClass($definition->getClass()); + foreach ($originalReflection->getMethods() as $method) { + if ($annotation = $annotationReader->getMethodAnnotation($method, Cache::class)) { + if ($method->isFinal()) { + throw new BadMethodCallException('Final methods can not be cached!'); + } + if ($method->isAbstract()) { + throw new BadMethodCallException('Abstract methods can not be cached!'); + } + if ($method->isStatic()) { + throw new BadMethodCallException('Static methods can not be cached!'); + } + + $factory = new Definition($definition->getClass()); + $factory->setFactory([new Reference('emag.cache.proxy.factory'), 'generate']); + $factory->setTags($definition->getTags()); + $factory->setArguments([$definition->getClass(), $definition->getArguments()]); + $factory->setMethodCalls($definition->getMethodCalls()); + $factory->setProperties($definition->getProperties()); + $factory->setProperties($definition->getProperties()); + $factory->addMethodCall('setReaderForCacheMethod', [new Reference("annotation_reader")]); + $factory->addMethodCall('setCacheServiceForMethod', [new Reference($container->getParameter('cache.service'))]); + $container->getDefinition('emag.cache.warmup')->addMethodCall('addClassToGenerate', [$definition->getClass()]); + + + $container->setDefinition($serviceId, $factory); + break; + } + } + } + + return $servicesToBeCached; + } + + +} \ No newline at end of file diff --git a/src/CacheBundle/DependencyInjection/CacheExtension.php b/src/CacheBundle/DependencyInjection/CacheExtension.php index e493387..0bfdd20 100644 --- a/src/CacheBundle/DependencyInjection/CacheExtension.php +++ b/src/CacheBundle/DependencyInjection/CacheExtension.php @@ -18,8 +18,12 @@ class CacheExtension extends Extension /** * @inheritdoc */ - public function load(array $config, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container) { - // TODO: Implement load() method. + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); } } diff --git a/src/CacheBundle/DependencyInjection/PointCut.php b/src/CacheBundle/DependencyInjection/PointCut.php deleted file mode 100644 index b14b828..0000000 --- a/src/CacheBundle/DependencyInjection/PointCut.php +++ /dev/null @@ -1,64 +0,0 @@ -reader = $reader; - } - - /** - * Determines whether the advice applies to instances of the given class. - * - * There are some limits as to what you can do in this method. Namely, you may - * only base your decision on resources that are part of the ContainerBuilder. - * Specifically, you may not use any data in the class itself, such as - * annotations. - * - * @param \ReflectionClass $class - * - * @return boolean - */ - public function matchesClass(\ReflectionClass $class) - { - foreach ($class->getMethods() as $method) { - $methodAnnotation = $this->reader->getMethodAnnotation($method, Cache::class); - if ($methodAnnotation) { - return true; - } - } - - return false; - } - - /** - * Determines whether the advice applies to the given method. - * - * This method is not limited in the way the matchesClass method is. It may - * use information in the associated class to make its decision. - * - * @param \ReflectionMethod $method - * - * @return boolean - */ - public function matchesMethod(\ReflectionMethod $method) - { - $methodAnnotation = $this->reader->getMethodAnnotation($method, Cache::class); - if ($methodAnnotation) { - return true; - } - - return false; - } -} \ No newline at end of file diff --git a/src/CacheBundle/DependencyInjection/ProxyWarmer.php b/src/CacheBundle/DependencyInjection/ProxyWarmer.php new file mode 100644 index 0000000..cefc96d --- /dev/null +++ b/src/CacheBundle/DependencyInjection/ProxyWarmer.php @@ -0,0 +1,53 @@ +factory = $factory; + } + + /** + * Checks whether this warmer is optional or not. + * + * Optional warmers can be ignored on certain conditions. + * + * A warmer should return true if the cache can be + * generated incrementally and on-demand. + * + * @return bool true if the warmer is optional, false otherwise + */ + public function isOptional() + { + return false; + } + + /** + * Warms up the cache. + * + * @param string $cacheDir The cache directory + */ + public function warmUp($cacheDir) + { + foreach ($this->classes as $class) { + $this->factory->createProxy($class); + } + } + + public function addClassToGenerate($className) + { + $this->classes[$className] = $className; + + } +} \ No newline at end of file diff --git a/src/CacheBundle/ProxyManager/CacheFactory.php b/src/CacheBundle/ProxyManager/CacheFactory.php new file mode 100644 index 0000000..8829658 --- /dev/null +++ b/src/CacheBundle/ProxyManager/CacheFactory.php @@ -0,0 +1,49 @@ +generator = $generator; + } + + public function setProxyConfig(\ProxyManager\Configuration $config) + { + $this->proxyConfig = $config; + } + + public function generate($class, $arguments = []) + { + $proxyClassName = $this->proxyConfig->getClassNameInflector()->getProxyClassName($class, [ + 'className' => $class, + 'factory' => ProxyCachingObjectFactory::class, + 'proxyManagerVersion' => \ProxyManager\Version::getVersion() + ]); + + if (!class_exists($proxyClassName)) { + $this->generator->createProxy($class); + } + $reflectionClass = new \ReflectionClass($proxyClassName); + if ($reflectionClass->hasMethod('__construct')) { + return ($reflectionClass)->newInstance($arguments); + } else { + return ($reflectionClass)->newInstance(); + } + + } + +} \ No newline at end of file diff --git a/src/CacheBundle/ProxyManager/CacheableClassTrait.php b/src/CacheBundle/ProxyManager/CacheableClassTrait.php new file mode 100644 index 0000000..e72e8f0 --- /dev/null +++ b/src/CacheBundle/ProxyManager/CacheableClassTrait.php @@ -0,0 +1,125 @@ +cacheServiceForMethod = $cacheServiceForMethod; + } + + /** + * @param AnnotationReader $readerForCacheMethod + */ + public function setReaderForCacheMethod(AnnotationReader $readerForCacheMethod) + { + $this->readerForCacheMethod = $readerForCacheMethod; + } + + public function getCached(\ReflectionMethod $method, $params) + { + /** @var Cache $annotation */ + $annotation = $this->readerForCacheMethod->getMethodAnnotation($method, Cache::class); + + $cacheKey = $this->getCacheKey($method, $params, $annotation); + + $cacheItem = $this->cacheServiceForMethod->getItem($cacheKey); + + if ($cacheItem->isHit() && !$annotation->isReset()) { + return $cacheItem->get(); + } + + $result = $method->invokeArgs($this, $params); + + $cacheItem->set($result); + $cacheItem->expiresAfter($annotation->getTtl()); + $this->cacheServiceForMethod->save($cacheItem); + + return $result; + } + + /** + * @param \ReflectionMethod $method + * @param $params + * @param Cache $cacheObj + * @return string + * @throws CacheException + */ + protected function getCacheKey(\ReflectionMethod $method, $params, Cache $cacheObj) + { + $refParams = $method->getParameters(); + $defaultParams = []; + foreach ($refParams as $id => $param) { + try { + $defaultValue = $param->getDefaultValue(); + $defaultParams[$id] = $defaultValue; + } catch (\ReflectionException $e) { + //do nothing + } + + } + + $arguments = $defaultParams; + + foreach ($refParams as $id => $param) { + if (array_key_exists($id, $params)) { + $arguments[$id] = $params[$id]; + } + } + + $cacheKey = ''; + if (empty($cacheObj->getKey())) { + $cacheKey = 'no_params_'; + } + + if (!empty($cacheObj->getKey())) { + $paramsToCache = array_map('trim', explode(',', $cacheObj->getKey())); + $paramsToCache = array_combine($paramsToCache, $paramsToCache); + + foreach ($refParams as $id => $param) { + if (in_array($param->getName(), $paramsToCache)) { + if (is_scalar($arguments[$id])) { + $cacheKey .= '_' . $arguments[$id]; + } else { + $cacheKey .= '_' . serialize($arguments[$id]); + } + unset($paramsToCache[$param->getName()]); + } + } + + if (!empty($paramsToCache)) { + throw new CacheException('Not all requested params can be used in cache key. Missing ' . implode(',', $paramsToCache)); + } + } + + $cacheKey = $cacheObj->getCache() . sha1($cacheKey); + + return $cacheKey; + } + + + +} \ No newline at end of file diff --git a/src/CacheBundle/ProxyManager/Factory/ProxyCachingObjectFactory.php b/src/CacheBundle/ProxyManager/Factory/ProxyCachingObjectFactory.php new file mode 100644 index 0000000..98d51eb --- /dev/null +++ b/src/CacheBundle/ProxyManager/Factory/ProxyCachingObjectFactory.php @@ -0,0 +1,24 @@ +generateProxy($className); + + return $proxyClassName; + } +} \ No newline at end of file diff --git a/src/CacheBundle/ProxyManager/Inflector/ClassNameInflector.php b/src/CacheBundle/ProxyManager/Inflector/ClassNameInflector.php new file mode 100644 index 0000000..5a5487e --- /dev/null +++ b/src/CacheBundle/ProxyManager/Inflector/ClassNameInflector.php @@ -0,0 +1,83 @@ + + * @license MIT + */ +final class ClassNameInflector implements ClassNameInflectorInterface +{ + /** + * @var string + */ + protected $proxyNamespace; + + /** + * @var int + */ + private $proxyMarkerLength; + + /** + * @var string + */ + private $proxyMarker; + + /** + * @var \ProxyManager\Inflector\Util\ParameterHasher + */ + private $parameterHasher; + + /** + * @param string $proxyNamespace + */ + public function __construct(string $proxyNamespace) + { + $this->proxyNamespace = $proxyNamespace; + $this->proxyMarker = '\\' . static::PROXY_MARKER . '\\'; + $this->proxyMarkerLength = strlen($this->proxyMarker); + $this->parameterHasher = new ParameterHasher(); + } + + /** + * {@inheritDoc} + */ + public function getUserClassName(string $className) : string + { + $className = ltrim($className, '\\'); + + if (false === $position = strrpos($className, $this->proxyMarker)) { + return $className; + } + + return substr( + $className, + $this->proxyMarkerLength + $position, + strrpos($className, '\\') - ($position + $this->proxyMarkerLength) + ); + } + + /** + * {@inheritDoc} + */ + public function getProxyClassName(string $className, array $options = []) : string + { + return $this->proxyNamespace + . $this->proxyMarker + . $this->getUserClassName($className) + . '\\Generated' . $this->parameterHasher->hashParameters($options); + } + + /** + * {@inheritDoc} + */ + public function isProxyClassName(string $className) : bool + { + return false !== strrpos($className, $this->proxyMarker); + } +} diff --git a/src/CacheBundle/ProxyManager/ProxyGenerator/CachedObjectGenerator.php b/src/CacheBundle/ProxyManager/ProxyGenerator/CachedObjectGenerator.php new file mode 100644 index 0000000..a5b40e7 --- /dev/null +++ b/src/CacheBundle/ProxyManager/ProxyGenerator/CachedObjectGenerator.php @@ -0,0 +1,55 @@ +setExtendedClass($originalClass->getName()); + $annotationReader = new AnnotationReader(); + $classGenerator->addTrait('\\' . CacheableClassTrait::class); + foreach ($originalClass->getMethods() as $method) + { + $annotation = $annotationReader->getMethodAnnotation($method, Cache::class); + if ($annotation) { + $parameters = []; + foreach ($method->getParameters() as $parameter) { + $parameters[] = "$".$parameter->getName(); + } + $parameters = implode(',', $parameters); + $body = <<getDeclaringClass()->getName()}', '{$method->getName()}'); + return \$this->getCached(\$ref, func_get_args()); +PHP; + $newm = MethodGenerator::fromReflection( + new MethodReflection( + $method->getDeclaringClass()->getName(), + $method->getName() + ) + ); + $newm->setDocBlock(""); + $newm->setBody($body); + $classGenerator->addMethodFromGenerator($newm); + } + } + } +} \ No newline at end of file diff --git a/src/CacheBundle/Resources/config/services.yml b/src/CacheBundle/Resources/config/services.yml new file mode 100644 index 0000000..54c4ebb --- /dev/null +++ b/src/CacheBundle/Resources/config/services.yml @@ -0,0 +1,28 @@ +parameters: + emag.cacheable.service.path: "%kernel.cache_dir%/aop-cache/proxies/" +services: + emag.cache.warmup: + class: CacheBundle\DependencyInjection\ProxyWarmer + calls: + - ['setFactory', ["@emag.cache.proxy.manager"]] + tags: + - { name: kernel.cache_warmer } + emag.cache.proxy.factory: + class: CacheBundle\CacheFactory + calls: + - ['setProxyFactory', ["@emag.cache.proxy.manager"]] + - ['setProxyConfig', ["@emag.cache.proxy.config"]] + emag.cache.proxy.manager: + class: CacheBundle\ProxyManager\Factory\ProxyCachingObjectFactory + arguments: ["@emag.cache.proxy.config"] + emag.cache.proxy.config: + class: ProxyManager\Configuration + calls: + - ['setProxiesTargetDir', ["%emag.cacheable.service.path%"]] + - ['setGeneratorStrategy', ["@emag.cache.proxy.persister"]] + emag.cache.proxy.persister: + class: ProxyManager\GeneratorStrategy\FileWriterGeneratorStrategy + arguments: ["@emag.cache.proxy.locator"] + emag.cache.proxy.locator: + class: ProxyManager\FileLocator\FileLocator + arguments: ["%emag.cacheable.service.path%"] \ No newline at end of file diff --git a/src/CacheBundle/Tests/CacheWarmupProxiesTest.php b/src/CacheBundle/Tests/CacheWarmupProxiesTest.php new file mode 100644 index 0000000..5d0374b --- /dev/null +++ b/src/CacheBundle/Tests/CacheWarmupProxiesTest.php @@ -0,0 +1,92 @@ +addCompilerPass($compilerPass); + parent::build($container); + } + }; + return [ + new \Symfony\Bundle\MonologBundle\MonologBundle(), + new \CacheBundle\CacheBundle(), + $dummyBundle, + ]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(__DIR__ . '/../../../app/config.yml'); + } + + public function __construct($environment, $debug) + { + parent::__construct($environment, $debug); + + $loader = require __DIR__ . '/../../../vendor/autoload.php'; + + AnnotationRegistry::registerLoader(array($loader, 'loadClass')); + $this->rootDir = __DIR__ . '/../../../app/'; + } + }); + } + + public function testClassCreated() + { + + self::bootKernel(['environment' => 'test_with_warmer']); + self::$kernel->getContainer()->get('cache_warmer') + ->warmup(self::$kernel->getContainer()->getParameter('kernel.cache_dir')); + + $filename = self::$kernel->getContainer()->get('emag.cache.proxy.config')->getClassNameInflector() + ->getProxyClassName(CacheableClass::class, [ + 'className' => CacheableClass::class, + 'factory' => ProxyCachingObjectFactory::class, + 'proxyManagerVersion' => \ProxyManager\Version::getVersion() + ]); + + $filename = str_replace("\\", '', $filename) . '.php'; + + $this->assertFileExists( + self::$kernel->getContainer()->getParameter('emag.cacheable.service.path') . $filename, + 'Cached file not created!' + ); + } +} \ No newline at end of file diff --git a/src/CacheBundle/Tests/CacheWrapperTest.php b/src/CacheBundle/Tests/CacheWrapperTest.php index 261519d..726f41f 100644 --- a/src/CacheBundle/Tests/CacheWrapperTest.php +++ b/src/CacheBundle/Tests/CacheWrapperTest.php @@ -1,9 +1,13 @@ container = self::$kernel->getContainer(); } + protected static function getKernelClass() + { + return get_class(new class('test', []) extends Kernel + { + + /** + * Returns an array of bundles to register. + * + * @return BundleInterface[] An array of bundle instances + */ + public function registerBundles() + { + return [ + new \Symfony\Bundle\MonologBundle\MonologBundle(), + new \CacheBundle\CacheBundle() + ]; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(__DIR__ . '/../../../app/config.yml'); + } + + public function __construct($environment, $debug) + { + parent::__construct($environment, $debug); + + $loader = require __DIR__ . '/../../../vendor/autoload.php'; + + AnnotationRegistry::registerLoader(array($loader, 'loadClass')); + $this->rootDir = __DIR__ . '/../../../app/'; + } + }); + } + public function testWithParams() { $object = $this->container->get('cache.testservice'); @@ -47,7 +86,6 @@ public function testReset() { $object = $this->container->get('cache.testservice'); - $data = $object->getCachedTime(); $this->assertEquals($data, $object->getCachedTime()); diff --git a/src/CacheBundle/Tests/ExtendableCacheableClass.php b/src/CacheBundle/Tests/ExtendableCacheableClass.php index 78ce0af..ce356db 100644 --- a/src/CacheBundle/Tests/ExtendableCacheableClass.php +++ b/src/CacheBundle/Tests/ExtendableCacheableClass.php @@ -18,4 +18,9 @@ public function getCachedTime($offset = 0) return rand(1 + $offset, microtime(true)); } + public function x(self $x) + { + //do stuff + } + } \ No newline at end of file