├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── Computaria │ └── Tardis │ ├── Factory.php │ ├── Identity │ ├── AlwaysTheSameValue.php │ ├── IdentityGenerator.php │ └── MethodCall.php │ ├── Interceptor │ ├── CacheFetch.php │ ├── CacheSave.php │ ├── PrefixInterceptor.php │ └── SufixInterceptor.php │ └── Proxy │ └── PublicMethods.php └── tests ├── Fixture └── TimeLord.php ├── Functional └── ArrayCacheTest.php └── Unit ├── Identity ├── AlwaysTheSameValueTest.php └── MethodCallTest.php ├── Interceptor ├── CacheFetchTest.php └── CacheSaveTest.php └── Proxy └── PublicMethodsTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | *.swp 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | php 3 | 4 | php: 5 | - 5.5 6 | - 5.6 7 | - 7 8 | 9 | cache: 10 | directories: 11 | - vendor 12 | 13 | before_script: 14 | - composer install --no-interaction --prefer-source 15 | 16 | script: 17 | - vendor/bin/phpunit --coverage-clover=coverage.clover 18 | 19 | after_script: 20 | - wget https://scrutinizer-ci.com/ocular.phar 21 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 22 | 23 | # vim: et ts=4 sw=4: 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Augusto Hagiro Pascutti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Computaria\Tardis: Cache made easier 2 | [![Build Status](https://travis-ci.org/computeiros/Tardis.svg?branch=develop)](https://travis-ci.org/computeiros/Tardis) 3 | 4 | Cache stuff with almost no modification on your code: 5 | 6 | ```php 7 | cacheCallsFrom($slowRepository); 17 | 18 | $brazil = $fastRepository->findByCode('BR'); 19 | ``` 20 | 21 | Requirements: PHP >= 5.5 22 | 23 | What happened above: 24 | 25 | 1. `$apc` is our cache storage. 26 | 2. `$tardis` is a factory of [proxies][proxy], caching into `$apc`. 27 | 3. `$tardis->cacheCallsFrom()` creates a new object which will cache and 28 | retrieve results from it. 29 | 30 | ## What cache storages are available? 31 | 32 | [Many][doctrine-cache]: 33 | 34 | - [Apc](http://php.net/apc) 35 | - [Couchbase](http://www.couchbase.com/) 36 | - Filesystem 37 | - [Memcache](http://php.net/manual/en/book.memcached.php) 38 | - [MongoDB](https://www.mongodb.org/) 39 | - [Redis](http://redis.io/) 40 | - [Riak](http://basho.com/riak/) 41 | - [SQLite](https://sqlite.org/) 42 | - [WinCache](http://php.net/wincache) 43 | - [ZendData](http://files.zend.com/help/Zend-Server/content/data_cache_component.htm) 44 | - [XCache](http://xcache.lighttpd.net/) 45 | 46 | ## Tardis limitations 47 | 48 | - We can't cache final classes. 49 | - We only cache public method calls. 50 | - Methods which accept [non-serializable][serialize] arguments can't 51 | be cached auto-magically. 52 | 53 | ## FAQ 54 | 55 | > Will the cache-enabled object Tardis create be of the same instance 56 | from the original object? 57 | 58 | Yes. The object created will be from a new class extending your original 59 | class. 60 | 61 | > Isn't creating a class on the fly expensive and slow? 62 | 63 | Yes, although not as slow as you might think and it is only a once in a 64 | time operation. 65 | 66 | > How are you sure of what cache key to retrieve? 67 | 68 | To create a cache key we use `Full\Class\Name::methodName` and serialize 69 | all its arguments, in order to have a unique identity to that call. 70 | 71 | If a method, of the same class and with the same arguments, return different 72 | things you are in trouble. 73 | 74 | > How do I expire a cache entry? 75 | 76 | You should use [Doctrine\Cache][doctrine-cache], or your own cache storage 77 | configuration for that. 78 | 79 | We are working on solutions for that. Have an idea? Help us! 80 | 81 | [proxy]: http://sourcemaking.com/design_patterns/proxy 82 | [doctrine-cache]: https://github.com/doctrine/cache 83 | [serialize]: http://php.net/serialize 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "phpunit/phpunit": "@stable" 4 | }, 5 | "autoload": { 6 | "psr-4": {"Computaria\\Tardis\\": "src/Computaria/Tardis"} 7 | }, 8 | "autoload-dev": { 9 | "psr-4": { "Computaria\\Tardis\\Tests\\": "tests" } 10 | }, 11 | "require": { 12 | "php": ">=5.5", 13 | "doctrine/cache": "~1.5.1", 14 | "ocramius/proxy-manager": "~1.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests/Functional 18 | 19 | 20 | 21 | tests/Unit 22 | 23 | 24 | 25 | 26 | 27 | src 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Computaria/Tardis/Factory.php: -------------------------------------------------------------------------------- 1 | setProxiesTargetDir($proxyDirectory); 36 | spl_autoload_register($proxyConfiguration->getProxyAutoloader()); 37 | 38 | return new ProxyManager\AccessInterceptorValueHolderFactory($proxyConfiguration); 39 | } 40 | 41 | private static function createIdentityGeneratorIfNecessary(IdentityGenerator $idGenerator=null) 42 | { 43 | if (false === is_null($idGenerator)) { 44 | return $idGenerator; 45 | } 46 | 47 | return new Identity\MethodCall; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Computaria/Tardis/Identity/AlwaysTheSameValue.php: -------------------------------------------------------------------------------- 1 | value = $returnAlwaysThisValue; 12 | } 13 | 14 | /** 15 | * @inherit 16 | */ 17 | public function createIdFor($methodName, array $methodArguments) 18 | { 19 | return $this->value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Computaria/Tardis/Identity/IdentityGenerator.php: -------------------------------------------------------------------------------- 1 | cacheAdapter = $cache; 22 | $this->identityGenerator = $idGenerator; 23 | } 24 | 25 | public function __invoke($proxy, $instance, $methodName, $methodArguments, &$returnEarly) 26 | { 27 | $cacheId = $this->identityGenerator->createIdFor($methodName, $methodArguments); 28 | if (false == $this->cacheAdapter->contains($cacheId)) { 29 | return; 30 | } 31 | 32 | $returnEarly = true; 33 | 34 | return $this->cacheAdapter->fetch($cacheId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Computaria/Tardis/Interceptor/CacheSave.php: -------------------------------------------------------------------------------- 1 | cacheAdapter = $cache; 22 | $this->identityGenerator = $idGenerator; 23 | } 24 | 25 | public function __invoke($proxy, $instance, $methodName, $methodArguments, $returnValue, &$returnEarly) 26 | { 27 | $cacheId = $this->identityGenerator->createIdFor($methodName, $methodArguments); 28 | if (false == $this->cacheAdapter->contains($cacheId)) { 29 | $this->cacheAdapter->save($cacheId, $returnValue); 30 | } 31 | 32 | return $returnValue; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Computaria/Tardis/Interceptor/PrefixInterceptor.php: -------------------------------------------------------------------------------- 1 | proxyFactory = $proxyFactory; 44 | $this->prefixInterceptor = $prefixInterceptor; 45 | $this->sufixInterceptor = $sufixInterceptor; 46 | } 47 | 48 | public function cacheCallsFrom($instance) 49 | { 50 | $methodNames = $this->getAllPublicMethodsFromInstance($instance); 51 | $prefixInterceptors = array_map($this->apply($this->prefixInterceptor), $methodNames); 52 | $sufixInterceptors = array_map($this->apply($this->sufixInterceptor), $methodNames); 53 | 54 | return $this->proxyFactory->createProxy( 55 | $instance, 56 | $prefixInterceptors, 57 | $sufixInterceptors 58 | ); 59 | } 60 | 61 | private function apply($interceptor) 62 | { 63 | return function ($methodName) use ($interceptor) { 64 | return $interceptor; 65 | }; 66 | } 67 | 68 | /** 69 | * @TODO: Extract method to allow prior creation of proxy objects for production. 70 | */ 71 | private function getAllPublicMethodsFromInstance($instance) 72 | { 73 | $class = new ReflectionClass($instance); 74 | $allPublicMethods = ReflectionMethod::IS_PUBLIC; 75 | $methodNames = []; 76 | 77 | foreach ($class->getMethods($allPublicMethods) as $method) { 78 | $methodName = $method->getName(); 79 | if (in_array($methodName, $this->ignoreMethodsToProxy)) { 80 | continue; 81 | } 82 | 83 | $methodNames[$methodName] = $methodName; 84 | } 85 | 86 | return $methodNames; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Fixture/TimeLord.php: -------------------------------------------------------------------------------- 1 | name = $name; 13 | $this->greet = $greet; 14 | } 15 | 16 | public function salute($name = 'Impossible Girl') 17 | { 18 | $greeting = '%s %s! I\'m the %s!'; 19 | 20 | return sprintf($greeting, $this->greet, $name, $this->name); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Functional/ArrayCacheTest.php: -------------------------------------------------------------------------------- 1 | subject = new Fixture\TimeLord('Doctor', 'Allonzy'); 33 | 34 | $this->proxyFactory = new ProxyFactory; 35 | $this->cacheAdapter = new Cache\ArrayCache(); 36 | $this->cacheAdapter->save( 37 | self::EXISTING_CACHE_KEY, 38 | self::EXISTING_CACHE_VALUE 39 | ); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function fetches_cache_for_existing_key() 46 | { 47 | $idGenerator = new Tardis\Identity\AlwaysTheSameValue(self::EXISTING_CACHE_KEY); 48 | $tardis = Tardis\Factory::grow($this->cacheAdapter, $idGenerator, $this->proxyFactory); 49 | 50 | $proxiedSubject = $tardis->cacheCallsFrom($this->subject); 51 | 52 | $this->assertInstanceOf( 53 | get_class($this->subject), 54 | $proxiedSubject, 55 | 'The cache-able object should have the same type as the real subject class.' 56 | ); 57 | $this->assertEquals( 58 | $expectedResult = self::EXISTING_CACHE_VALUE, 59 | $proxiedSubject->salute(), 60 | 'Cached result of a method should return cache content.' 61 | ); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function unexisting_cache_creates_cache_entry() 68 | { 69 | $idGenerator = new Tardis\Identity\AlwaysTheSameValue(self::MISSING_CACHE_KEY); 70 | $tardis = Tardis\Factory::grow($this->cacheAdapter, $idGenerator, $this->proxyFactory); 71 | 72 | $proxiedSubject = $tardis->cacheCallsFrom($this->subject); 73 | $this->assertFalse( 74 | $this->cacheAdapter->contains(self::MISSING_CACHE_KEY), 75 | 'Cache should not contain missing key.' 76 | ); 77 | $this->assertInstanceOf( 78 | get_class($this->subject), 79 | $proxiedSubject, 80 | 'The cache-able object should have the same type as the real subject class.' 81 | ); 82 | 83 | $proxiedSubject->salute(); 84 | 85 | $this->assertTrue( 86 | $this->cacheAdapter->contains(self::MISSING_CACHE_KEY), 87 | 'Cache should contain missing key after method is called on proxy.' 88 | ); 89 | $this->assertEquals( 90 | $this->subject->salute(), 91 | $this->cacheAdapter->fetch(self::MISSING_CACHE_KEY), 92 | 'Value put on cache should be the same one returned by the real subject.' 93 | ); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function can_grow_without_identity_generator() 100 | { 101 | $noIdGenerator = null; 102 | 103 | $tardis = Tardis\Factory::grow($this->cacheAdapter, $noIdGenerator, $this->proxyFactory); 104 | 105 | $this->assertInstanceOf( 106 | 'Computaria\Tardis\Proxy\PublicMethods', 107 | $tardis, 108 | 'A public method proxy factory should be able to be created without and identity generator.' 109 | ); 110 | } 111 | 112 | /** 113 | * @test 114 | */ 115 | public function can_grow_only_with_cache() 116 | { 117 | $noIdGenerator = null; 118 | $noProxyFactory = null; 119 | 120 | $tardis = Tardis\Factory::grow($this->cacheAdapter, $noIdGenerator, $noProxyFactory); 121 | 122 | $this->assertInstanceOf( 123 | 'Computaria\Tardis\Proxy\PublicMethods', 124 | $tardis, 125 | 'A public method proxy factory should be able to be created without and identity generator.' 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Unit/Identity/AlwaysTheSameValueTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 18 | $expectedValue, 19 | $identityGenerator->createIdFor($methodName, $methodArguments), 20 | 'The "always" identity generator strategy should always return the same value.' 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Unit/Identity/MethodCallTest.php: -------------------------------------------------------------------------------- 1 | generator = new MethodCall; 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function identity_for_same_method_and_arguments_should_be_the_same() 21 | { 22 | $methodName = 'travelTo'; 23 | $methodArguments = ['2015AD', '1018AD']; 24 | 25 | $firstIdentity = $this->generator->createIdFor($methodName, $methodArguments); 26 | $secondIdentity = $this->generator->createIdFor($methodName, $methodArguments); 27 | 28 | $this->assertEquals( 29 | $firstIdentity, 30 | $secondIdentity, 31 | 'Given the same arguments, identity should be the same.' 32 | ); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function identity_for_different_arguments_should_be_different() 39 | { 40 | $firstIdentity = $this->generator->createIdFor('createFromFormat', ['d', '15']); 41 | $secondIdentity = $this->generator->createIdFor('createFromFormat', ['m', '12']); 42 | 43 | $this->assertNotEquals( 44 | $firstIdentity, 45 | $secondIdentity, 46 | 'Different method calls (with different arguments) should result in different identities.' 47 | ); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function identity_for_method_arguments_that_cannot_be_serialized() 54 | { 55 | $this->markTestSkipped('A unserializable argument is not currently supported.'); 56 | 57 | $unserializableArgument = new \Pdo('sqlite::memory:'); 58 | $methodName = 'createIdForUnserializableArgument'; 59 | $methodArguments = [$unserializableArgument]; 60 | 61 | $firstIdentity = $this->generator->createIdFor($methodName, $methodArguments); 62 | $secondIdentity = $this->generator->createIdFor($methodName, $methodArguments); 63 | 64 | $this->assertEquals( 65 | $firstIdentity, 66 | $secondIdentity, 67 | 'Given the same arguments, identity should be the same.' 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Unit/Interceptor/CacheFetchTest.php: -------------------------------------------------------------------------------- 1 | 'Amy Pond']; 30 | /** 31 | * @var boolean 32 | */ 33 | private $returnEarly = false; 34 | 35 | public function setUp() 36 | { 37 | $this->real = new Fixture\TimeLord( 38 | 'Master', 39 | 'Oh, it is you again' 40 | ); 41 | 42 | $this->proxy = clone $this->real; 43 | 44 | $allMethods = []; 45 | $this->cacheAdapter = $this->getMock( 46 | 'Doctrine\Common\Cache\Cache', 47 | $allMethods 48 | ); 49 | 50 | $this->identityGenerator = new Identity\AlwaysTheSameValue(self::EXISTING_KEY); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function non_cached_value_does_not_get_returned() 57 | { 58 | $this->cacheAdapter->expects($this->once()) 59 | ->method('contains') 60 | ->with(self::EXISTING_KEY) 61 | ->willReturn(false); 62 | $this->cacheAdapter->expects($this->never()) 63 | ->method('fetch'); 64 | 65 | $sufixInterceptor = new CacheFetch($this->cacheAdapter, $this->identityGenerator); 66 | $returnedValue = $sufixInterceptor($this->proxy, $this->real, $this->methodName, $this->methodArguments, $this->returnEarly); 67 | 68 | $this->assertEmpty( 69 | $returnedValue, 70 | 'Value not on cache should not get returned.' 71 | ); 72 | $this->assertFalse( 73 | $this->returnEarly, 74 | 'A non cached value should not get returned early, procceding the call on the real object.' 75 | ); 76 | } 77 | 78 | /** 79 | * @test 80 | */ 81 | public function cached_value_gets_returned_early() 82 | { 83 | $this->cacheAdapter->expects($this->once()) 84 | ->method('contains') 85 | ->with(self::EXISTING_KEY) 86 | ->willReturn(true); 87 | $this->cacheAdapter->expects($this->once()) 88 | ->method('fetch') 89 | ->with(self::EXISTING_KEY) 90 | ->willReturn(self::EXISTING_VALUE); 91 | 92 | $sufixInterceptor = new CacheFetch($this->cacheAdapter, $this->identityGenerator); 93 | $returnedValue = $sufixInterceptor($this->proxy, $this->real, $this->methodName, $this->methodArguments, $this->returnEarly); 94 | 95 | $this->assertEquals( 96 | self::EXISTING_VALUE, 97 | $returnedValue, 98 | 'Value returned should be the same one cached.' 99 | ); 100 | $this->assertTrue( 101 | $this->returnEarly, 102 | 'Value on cache should be returned early.' 103 | ); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /tests/Unit/Interceptor/CacheSaveTest.php: -------------------------------------------------------------------------------- 1 | 'Amy Pond']; 30 | /** 31 | * @var boolean 32 | */ 33 | private $returnEarly = false; 34 | 35 | public function setUp() 36 | { 37 | $this->real = new Fixture\TimeLord( 38 | 'Omega', 39 | 'I\'m back from the other universe' 40 | ); 41 | 42 | $this->proxy = clone $this->real; 43 | 44 | $allMethods = []; 45 | $this->cacheAdapter = $this->getMock( 46 | 'Doctrine\Common\Cache\Cache', 47 | $allMethods 48 | ); 49 | 50 | $this->identityGenerator = new Identity\AlwaysTheSameValue(self::EXISTING_KEY); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function value_gets_cached() 57 | { 58 | $this->cacheAdapter->expects($this->once()) 59 | ->method('contains') 60 | ->with(self::EXISTING_KEY) 61 | ->willReturn(false); 62 | $this->cacheAdapter->expects($this->once()) 63 | ->method('save') 64 | ->with(self::EXISTING_KEY, self::EXISTING_VALUE); 65 | 66 | $sufixInterceptor = new CacheSave($this->cacheAdapter, $this->identityGenerator); 67 | $returnValue = self::EXISTING_VALUE; 68 | $returnedValue = $sufixInterceptor($this->proxy, $this->real, $this->methodName, $this->methodArguments, $returnValue, $this->returnEarly); 69 | 70 | $this->assertEquals( 71 | $returnValue, 72 | $returnedValue, 73 | 'Value returned should be the same one returned form the real object.' 74 | ); 75 | $this->assertFalse( 76 | $this->returnEarly, 77 | 'Sufix interceptor should never return an early value.' 78 | ); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function cached_value_does_not_get_cached_again() 85 | { 86 | $this->cacheAdapter->expects($this->once()) 87 | ->method('contains') 88 | ->with(self::EXISTING_KEY) 89 | ->willReturn(true); 90 | $this->cacheAdapter->expects($this->never()) 91 | ->method('save'); 92 | 93 | $sufixInterceptor = new CacheSave($this->cacheAdapter, $this->identityGenerator); 94 | $returnValue = self::EXISTING_VALUE; 95 | $returnedValue = $sufixInterceptor($this->proxy, $this->real, $this->methodName, $this->methodArguments, $returnValue, $this->returnEarly); 96 | 97 | $this->assertEquals( 98 | $returnValue, 99 | $returnedValue, 100 | 'Value returned should be the same one returned form the real object.' 101 | ); 102 | $this->assertFalse( 103 | $this->returnEarly, 104 | 'Sufix interceptor should never return an early value.' 105 | ); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /tests/Unit/Proxy/PublicMethodsTest.php: -------------------------------------------------------------------------------- 1 | proxyFactory = $this->getMockForAbstractClass( 33 | 'ProxyManager\Factory\AbstractBaseFactory', 34 | $constructorArguments = [], 35 | $className = '', 36 | $callOriginalConstructor = true, 37 | $callOriginalClone = true, 38 | $useAutoload = true, 39 | ['createProxy'] 40 | ); 41 | $this->prefixInterceptor = $this->getMock('Computaria\\Tardis\\Interceptor\\PrefixInterceptor'); 42 | $this->sufixIntercetor = $this->getMock('Computaria\\Tardis\\Interceptor\\SufixInterceptor'); 43 | 44 | $this->factory = new PublicMethods( 45 | $this->proxyFactory, 46 | $this->prefixInterceptor, 47 | $this->sufixIntercetor 48 | ); 49 | } 50 | 51 | /** 52 | * @test 53 | */ 54 | public function can_create_proxy_from_instances() 55 | { 56 | $realInstance = new Fixture\TimeLord('Rassilion', 'I\'m the first and only'); 57 | 58 | $this->proxyFactory->expects($this->once()) 59 | ->method('createProxy') 60 | ->with( 61 | $realInstance, 62 | $this->contains($this->prefixInterceptor), 63 | $this->contains($this->sufixIntercetor) 64 | ); 65 | 66 | $proxiedInstance = $this->factory->cacheCallsFrom($realInstance); 67 | } 68 | } 69 | --------------------------------------------------------------------------------