├── .php_cs ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── appveyor.yml ├── composer.json ├── composer.lock ├── config └── bootstrap.php ├── src ├── Annotations │ ├── Async.php │ ├── Ignore.php │ └── Sync.php ├── AsyncTable.php ├── AsyncTableGenerator.php ├── AsyncTableInterface.php ├── AsyncTableRegistry.php ├── GeneratedTable.php ├── Pool.php ├── Shell │ └── GenerateShell.php ├── TableRegistryTrait.php └── WorkerChild.php └── test_app ├── ExpectedGeneratedAsyncTable └── C17a66dcf052f6878c3f1c553db4d6bd0_Ff47f6f78cf1b377de64788b3705cda9c.php └── Table └── ScreenshotsTable.php /.php_cs: -------------------------------------------------------------------------------- 1 | setFinder( 15 | PhpCsFixer\Finder::create() 16 | ->in($paths) 17 | ->append($paths) 18 | ) 19 | ->setUsingCache(false) 20 | ; 21 | })(); 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are highly appreciated. Here's a quick guide. 4 | 5 | Fork, then clone the repo: 6 | 7 | git clone git@github.com:your-username/cakephp-async-orm.git 8 | 9 | Set up your machine: 10 | 11 | composer install 12 | 13 | Make sure the tests pass: 14 | 15 | make unit 16 | 17 | Make sure the tests pass on all supported PHP versions (requires docker): 18 | 19 | make dunit 20 | 21 | Make your change. Add tests for your change. Make the tests pass: 22 | 23 | make dunit && make unit 24 | 25 | Before committing and submitting your pull request make sure it passes PSR2 coding style, unit tests pass and pass on all supported PHP versions: 26 | 27 | make contrib 28 | 29 | Push to your fork and [submit a pull request][pr]. 30 | 31 | [pr]: https://help.github.com/articles/creating-a-pull-request/ 32 | 33 | At this point you're waiting on me. I like to at least comment on pull requests 34 | within a day or two. I may suggest some changes or improvements or alternatives. 35 | 36 | Some things that will increase the chance that your pull request is accepted: 37 | 38 | * Write tests. 39 | * Follow PSR2 (travis will also check for this). 40 | * Write a [good commit message][commit]. 41 | 42 | [commit]: http://chris.beams.io/posts/git-commit/ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cees-Jan Kiewiet 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | composer run-script qa-all --timeout=0 3 | 4 | all-coverage: 5 | composer run-script qa-all-coverage --timeout=0 6 | 7 | ci: 8 | composer run-script qa-ci --timeout=0 9 | 10 | ci-extended: 11 | composer run-script qa-ci-extended --timeout=0 12 | 13 | contrib: 14 | composer run-script qa-contrib --timeout=0 15 | 16 | init: 17 | composer ensure-installed 18 | 19 | cs: 20 | composer cs 21 | 22 | cs-fix: 23 | composer cs-fix 24 | 25 | unit: 26 | composer run-script unit --timeout=0 27 | 28 | unit-coverage: 29 | composer run-script unit-coverage --timeout=0 30 | 31 | ci-coverage: init 32 | composer ci-coverage 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cakephp-async-orm 2 | 3 | [![Linux Build Status](https://travis-ci.org/WyriHaximus/cakephp-async-orm.png)](https://travis-ci.org/WyriHaximus/cakephp-async-orm) 4 | [![Latest Stable Version](https://poser.pugx.org/WyriHaximus/cake-async-orm/v/stable.png)](https://packagist.org/packages/WyriHaximus/cake-async-orm) 5 | [![Total Downloads](https://poser.pugx.org/wyrihaximus/cake-async-orm/downloads.png)](https://packagist.org/packages/wyrihaximus/cake-async-orm) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/WyriHaximus/cakephp-async-orm/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/WyriHaximus/cakephp-async-orm/?branch=master) 7 | [![License](https://poser.pugx.org/wyrihaximus/cake-async-orm/license.png)](https://packagist.org/packages/wyrihaximus/cake-async-orm) 8 | [![PHP 7 ready](http://php7ready.timesplinter.ch/WyriHaximus/cakephp-async-orm/badge.svg)](https://travis-ci.org/WyriHaximus/cakephp-async-orm) 9 | 10 | Asynchronous access to cake3 models in async projecs, currently in early Alpha stage. 11 | 12 | # Example 13 | 14 | ```php 15 | find('all')->then(function ($data) use ($loop) { 32 | var_export($data); 33 | $loop->stop(); 34 | }, function ($error) use ($loop) { 35 | var_export($error); 36 | $loop->stop(); 37 | }); 38 | 39 | $loop->run(); 40 | } 41 | } 42 | ``` 43 | 44 | # (A)sync detection 45 | 46 | In order to only run the necessary calls to the table object on the pool several detection strategies have been put in place, namely: 47 | 48 | * Docblock return type, if it matches `Cake\ORM\Query` it will ignore any annotations or function names 49 | * Annotations Async and Sync can be used class wide but overwritten on the method level 50 | * Function name detection, `fetch*`, `find*`, and `retrieve*` will be async and the rest sync unless overwritten by annotations or return type 51 | 52 | ## License ## 53 | 54 | Copyright 2015 [Cees-Jan Kiewiet](http://wyrihaximus.net/) 55 | 56 | Permission is hereby granted, free of charge, to any person 57 | obtaining a copy of this software and associated documentation 58 | files (the "Software"), to deal in the Software without 59 | restriction, including without limitation the rights to use, 60 | copy, modify, merge, publish, distribute, sublicense, and/or sell 61 | copies of the Software, and to permit persons to whom the 62 | Software is furnished to do so, subject to the following 63 | conditions: 64 | 65 | The above copyright notice and this permission notice shall be 66 | included in all copies or substantial portions of the Software. 67 | 68 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 69 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 70 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 71 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 72 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 73 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 74 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 75 | OTHER DEALINGS IN THE SOFTWARE. 76 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | platform: 3 | - x64 4 | clone_folder: c:\projects\php-project-workspace 5 | 6 | ## Build matrix for lowest and highest possible targets 7 | environment: 8 | matrix: 9 | - dependencies: lowest 10 | php_ver_target: 7.2 11 | - dependencies: current 12 | php_ver_target: 7.2 13 | - dependencies: highest 14 | php_ver_target: 7.2 15 | 16 | ## Cache composer, chocolatey and php bits 17 | cache: 18 | - '%LOCALAPPDATA%\Composer\files -> composer.lock' 19 | 20 | ## Set up environment varriables 21 | init: 22 | - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% 23 | - SET COMPOSER_NO_INTERACTION=1 24 | - SET PHP=1 25 | - SET ANSICON=121x90 (121x90) 26 | 27 | ## Install PHP and composer, and run the appropriate composer command 28 | install: 29 | - IF EXIST c:\tools\php (SET PHP=0) 30 | - ps: appveyor-retry cinst --params '""/InstallDir:C:\tools\php""' --ignore-checksums -y php --version ((choco search php --exact --all-versions -r | select-string -pattern $env:php_ver_target | sort { [version]($_ -split '\|' | select -last 1) } -Descending | Select-Object -first 1) -replace '[php|]','') 31 | - cd c:\tools\php 32 | - IF %PHP%==1 copy php.ini-production php.ini /Y 33 | - IF %PHP%==1 echo date.timezone="UTC" >> php.ini 34 | - IF %PHP%==1 echo extension_dir=ext >> php.ini 35 | - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini 36 | - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini 37 | - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini 38 | - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat 39 | - appveyor-retry appveyor DownloadFile https://getcomposer.org/composer.phar 40 | - cd c:\projects\php-project-workspace 41 | - composer config --unset platform.php 42 | - IF %dependencies%==lowest appveyor-retry composer update --prefer-lowest --no-progress --profile -n 43 | - IF %dependencies%==current appveyor-retry composer install --no-progress --profile 44 | - IF %dependencies%==highest appveyor-retry composer update --no-progress --profile -n 45 | - composer show 46 | 47 | ## Run the actual test 48 | test_script: 49 | - cd c:\projects\php-project-workspace 50 | - vendor/bin/phpunit -c phpunit.xml.dist 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wyrihaximus/cake-async-orm", 3 | "description": "Asynchronous CakePHP ORM", 4 | "type":"cakephp-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Cees-Jan Kiewiet", 9 | "email": "ceesjank@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.2", 14 | "api-clients/cs-fixer-config": "^1.0", 15 | "cakephp/cakephp": "^3.7.2", 16 | "doctrine/annotations": "^1.2", 17 | "friendsofphp/php-cs-fixer": "^2.14", 18 | "nikic/php-parser": "^4.0 || ^3.0 || ^2.0 || ^1.0", 19 | "phpdocumentor/reflection-docblock": "^4.0 || ^3.0 || ^2.0", 20 | "roave/better-reflection": "^3.1 || ^2.0", 21 | "wyrihaximus/react-child-process-pool": "^1.3" 22 | }, 23 | "require-dev": { 24 | "api-clients/test-utilities": "^5.4" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "WyriHaximus\\React\\Cake\\Orm\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "WyriHaximus\\React\\Tests\\Cake\\Orm\\": "tests/", 34 | "WyriHaximus\\React\\TestApp\\Cake\\Orm\\": "test_app/" 35 | } 36 | }, 37 | "config": { 38 | "sort-packages": true, 39 | "platform": { 40 | "php": "7.2" 41 | } 42 | }, 43 | "scripts": { 44 | "ensure-installed": "composer install --ansi -n -q", 45 | "cs": [ 46 | "@ensure-installed", 47 | "php-cs-fixer fix --config=.php_cs --ansi --dry-run --diff --verbose --allow-risky=yes --show-progress=estimating" 48 | ], 49 | "cs-fix": [ 50 | "@ensure-installed", 51 | "php-cs-fixer fix --config=.php_cs --ansi --verbose --allow-risky=yes --show-progress=estimating" 52 | ], 53 | "unit": [ 54 | "@ensure-installed", 55 | "phpunit --colors=always -c phpunit.xml.dist" 56 | ], 57 | "unit-coverage": [ 58 | "@ensure-installed", 59 | "phpunit --colors=always -c phpunit.xml.dist --coverage-text --coverage-html covHtml --coverage-clover ./build/logs/clover.xml" 60 | ], 61 | "lint-php": [ 62 | "@ensure-installed", 63 | "parallel-lint --exclude vendor ." 64 | ], 65 | "qa-all": [ 66 | "@lint-php", 67 | "@cs", 68 | "@unit" 69 | ], 70 | "qa-all-coverage": [ 71 | "@lint-php", 72 | "@cs", 73 | "@unit-coverage" 74 | ], 75 | "qa-windows": [ 76 | "@lint-php", 77 | "@cs", 78 | "@unit" 79 | ], 80 | "qa-ci": [ 81 | "@unit" 82 | ], 83 | "qa-ci-extended": [ 84 | "@qa-all-coverage" 85 | ], 86 | "qa-ci-windows": [ 87 | "@qa-windows" 88 | ], 89 | "qa-contrib": [ 90 | "@qa-all" 91 | ], 92 | "ci-coverage": [ 93 | "if [ -f ./build/logs/clover.xml ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover ./build/logs/clover.xml; fi" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'WyriHaximus\React\ChildProcess\Messenger\Messages\SecureLine', 9 | 'lineOptions' => [ 10 | 'key' => 'CHANGETHISTOSOMETHINGSAFE!!!!!!9^(%!@#*T!@*&G!*@^&ET', 11 | ], 12 | ]); 13 | } 14 | 15 | if (!Configure::check('WyriHaximus.React.Cake.Orm.TTL')) { 16 | Configure::write('WyriHaximus.React.Cake.Orm.TTL', 3); 17 | } 18 | 19 | if (!Configure::check('WyriHaximus.React.Cake.Orm.Cache.AsyncTables')) { 20 | Configure::write('WyriHaximus.React.Cake.Orm.Cache.AsyncTables', CACHE . 'asyncTables' . DS); 21 | } 22 | 23 | AnnotationRegistry::registerLoader(function ($class) { 24 | return class_exists($class); 25 | }); 26 | -------------------------------------------------------------------------------- /src/Annotations/Async.php: -------------------------------------------------------------------------------- 1 | pool = $pool; 44 | $this->tableName = $tableName; 45 | $this->annotationReader = new AnnotationReader(); 46 | $this->reflectionClass = new \ReflectionClass($tableClass); 47 | } 48 | 49 | /** 50 | * @param $function 51 | * @param array $arguments 52 | * @return PromiseInterface 53 | */ 54 | protected function callAsyncOrSync($function, $arguments) 55 | { 56 | if ($this->pool === null) { 57 | return (new $this->tableName())->$function(...$arguments); 58 | } 59 | 60 | if ( 61 | $this->returnsQuery($function) || 62 | $this->hasMethodAnnotation($function, Async::class) || 63 | ( 64 | $this->hasClassAnnotation(Async::class) && 65 | $this->hasNoMethodAnnotation($function) 66 | ) || 67 | strpos(strtolower($function), 'save') === 0 || 68 | strpos(strtolower($function), 'find') === 0 || 69 | strpos(strtolower($function), 'fetch') === 0 || 70 | strpos(strtolower($function), 'retrieve') === 0 71 | ) { 72 | return $this->callAsync($function, $arguments); 73 | } 74 | 75 | return $this->callSync($function, $arguments); 76 | } 77 | 78 | /** 79 | * @param $function 80 | * @param array $arguments 81 | * @return PromiseInterface 82 | */ 83 | private function callSync($function, array $arguments = []) 84 | { 85 | $table = TableRegistry::get($this->tableName); 86 | if (isset(class_uses($table)[TableRegistryTrait::class])) { 87 | $table->setRegistry(AsyncTableRegistry::class); 88 | } 89 | 90 | return \React\Promise\resolve( 91 | call_user_func_array( 92 | [ 93 | $table, 94 | $function, 95 | ], 96 | $arguments 97 | ) 98 | ); 99 | } 100 | 101 | /** 102 | * @param $function 103 | * @param array $arguments 104 | * @return PromiseInterface 105 | */ 106 | private function callAsync($function, array $arguments = []) 107 | { 108 | $unSerialize = function ($input) { 109 | if (is_string($input)) { 110 | return unserialize($input); 111 | } 112 | 113 | return $input; 114 | }; 115 | 116 | return $this-> 117 | pool-> 118 | call(get_parent_class($this), $this->tableName, $function, $arguments)-> 119 | then($unSerialize, $unSerialize, $unSerialize) 120 | ; 121 | } 122 | 123 | /** 124 | * @param $class 125 | * @return bool 126 | */ 127 | private function hasClassAnnotation($class) 128 | { 129 | return is_a($this->annotationReader->getClassAnnotation($this->reflectionClass, $class), $class); 130 | } 131 | 132 | /** 133 | * @param $method 134 | * @param $class 135 | * @return bool 136 | */ 137 | private function hasMethodAnnotation($method, $class) 138 | { 139 | $methodReflection = $this->reflectionClass->getMethod($method); 140 | 141 | return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class); 142 | } 143 | 144 | /** 145 | * @param $method 146 | * @return bool 147 | */ 148 | private function hasNoMethodAnnotation($method) 149 | { 150 | $methodReflection = $this->reflectionClass->getMethod($method); 151 | 152 | return ( 153 | $this->annotationReader->getMethodAnnotation($methodReflection, Async::class) === null && 154 | $this->annotationReader->getMethodAnnotation($methodReflection, Sync::class) === null 155 | ); 156 | } 157 | 158 | /** 159 | * @param $function 160 | * @return bool 161 | */ 162 | private function returnsQuery($function) 163 | { 164 | $docBlockContents = $this->reflectionClass->getMethod($function)->getDocComment(); 165 | if (!is_string($docBlockContents)) { 166 | return false; 167 | } 168 | 169 | $docBlock = $this->getDocBlock($docBlockContents); 170 | foreach ($docBlock->getTags() as $tag) { 171 | if ($tag->getName() === 'return' && ltrim($tag->getType(), '\\') == Query::class) { 172 | return true; 173 | } 174 | } 175 | 176 | return false; 177 | } 178 | 179 | /** 180 | * @param $docBlockContents 181 | * @return DocBlock 182 | */ 183 | private function getDocBlock($docBlockContents) 184 | { 185 | if (class_exists('phpDocumentor\Reflection\DocBlockFactory')) { 186 | return DocBlockFactory::createInstance()->create($docBlockContents); 187 | } 188 | 189 | return new DocBlock($docBlockContents); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/AsyncTableGenerator.php: -------------------------------------------------------------------------------- 1 | storageLocation = $storageLocation; 54 | $this->factory = new BuilderFactory(); 55 | $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 56 | $this->classLoader = $this->locateClassloader(); 57 | $this->annotationReader = new AnnotationReader(); 58 | } 59 | 60 | /** 61 | * @param string $tableClass 62 | * @param bool $force 63 | * @return GeneratedTable 64 | */ 65 | public function generate($tableClass, $force = false) 66 | { 67 | $fileName = $this->classLoader->findFile($tableClass); 68 | $contents = file_get_contents($fileName); 69 | $ast = $this->parser->parse($contents); 70 | $namespace = static::NAMESPACE_PREFIX; 71 | 72 | $hashedClass = $this->extractNamespace($ast) . '_C' . md5($tableClass) . '_F' . md5($contents); 73 | 74 | $generatedTable = new GeneratedTable($namespace, $hashedClass); 75 | 76 | if (!$force && file_exists($this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php')) { 77 | return $generatedTable; 78 | } 79 | 80 | $class = $this->factory->class($hashedClass) 81 | ->extend('BaseTable') 82 | ->implement('AsyncTableInterface') 83 | ; 84 | 85 | $class->addStmt( 86 | new Node\Stmt\TraitUse([ 87 | new Node\Name('AsyncTable'), 88 | ]) 89 | ); 90 | 91 | $class->addStmt( 92 | self::createMethod( 93 | 'save', 94 | [ 95 | new Node\Param('entity', null, 'EntityInterface'), 96 | new Node\Param('options', new Node\Expr\Array_()), 97 | ] 98 | ) 99 | ); 100 | 101 | foreach ($this->extractMethods($ast) as $method) { 102 | if (in_array($method->name, ['initialize', 'validationDefault'], true)) { 103 | continue; 104 | } 105 | 106 | if ($this->hasMethodAnnotation(new ReflectionClass($tableClass), $method->name, Ignore::class)) { 107 | continue; 108 | } 109 | 110 | $class->addStmt( 111 | self::createMethod( 112 | $method->name, 113 | $method->params 114 | ) 115 | ); 116 | } 117 | 118 | $uses = iterator_to_array($this->extractClassImports($ast)); 119 | $uses[] = $this->factory->use(EntityInterface::class); 120 | $uses[] = $this->factory->use($tableClass)->as('BaseTable'); 121 | $uses[] = $this->factory->use(AsyncTable::class); 122 | $uses[] = $this->factory->use(AsyncTable::class); 123 | $uses[] = $this->factory->use(AsyncTableInterface::class); 124 | 125 | $node = $this->factory->namespace($namespace) 126 | ->addStmts($this->removeDuplicatedUses($uses)) 127 | ->addStmt($class) 128 | ->getNode() 129 | ; 130 | 131 | $fileName = $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php'; 132 | $prettyPrinter = new Standard(); 133 | $fileContents = $prettyPrinter->prettyPrintFile([$node,]) . PHP_EOL; 134 | file_put_contents( 135 | $fileName, 136 | $fileContents 137 | ); 138 | 139 | do { 140 | usleep(500); 141 | } while (file_get_contents($fileName) !== $fileContents); 142 | 143 | $command = 'PHP_CS_FIXER_IGNORE_ENV=1 ' . 144 | dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor/bin/php-cs-fixer fix ' . 145 | $this->storageLocation . DIRECTORY_SEPARATOR . $hashedClass . '.php' . 146 | ' --config=' . 147 | dirname(__DIR__) . 148 | DIRECTORY_SEPARATOR . 149 | '.php_cs ' . 150 | ' --allow-risky=yes -q -v --stop-on-violation --using-cache=no' . 151 | ' 2>&1'; 152 | 153 | exec($command); 154 | 155 | return $generatedTable; 156 | } 157 | 158 | protected function removeDuplicatedUses(array $rawUses) 159 | { 160 | $uses = []; 161 | /** @var Node\Stmt\Use_ $use */ 162 | foreach ($rawUses as $use) { 163 | if ($use instanceof Use_) { 164 | $use = $use->getNode(); 165 | } 166 | 167 | $uses[$use->uses[0]->type . '_____' . $use->uses[0]->name->toString() . '_____' . $use->uses[0]->alias] = $use; 168 | } 169 | 170 | return $uses; 171 | } 172 | 173 | protected function createMethod($method, array $params) 174 | { 175 | return $this->factory->method($method) 176 | ->makePublic() 177 | ->addParams($params) 178 | ->addStmt( 179 | new Node\Stmt\Return_( 180 | new Node\Expr\MethodCall( 181 | new Node\Expr\Variable('this'), 182 | 'callAsyncOrSync', 183 | [ 184 | new Node\Scalar\String_($method), 185 | new Node\Expr\Array_( 186 | $this->createMethodArguments($params) 187 | ), 188 | ] 189 | ) 190 | ) 191 | ) 192 | ; 193 | } 194 | 195 | /** 196 | * @param array $params 197 | * @return array 198 | */ 199 | protected function createMethodArguments(array $params) 200 | { 201 | $arguments = []; 202 | foreach ($params as $param) { 203 | if (!($param instanceof Node\Param)) { 204 | continue; 205 | } 206 | $arguments[] = new Node\Expr\Variable($param->name); 207 | } 208 | 209 | return $arguments; 210 | } 211 | 212 | /** 213 | * @param Node[] $ast 214 | * @return Generator 215 | */ 216 | protected function extractMethods(array $ast) 217 | { 218 | foreach ($ast as $node) { 219 | if (!isset($node->stmts)) { 220 | continue; 221 | } 222 | 223 | foreach ($this->iterageStmts($node->stmts) as $stmt) { 224 | yield $stmt; 225 | } 226 | } 227 | } 228 | 229 | protected function iterageStmts(array $stmts) 230 | { 231 | foreach ($stmts as $stmt) { 232 | if ($stmt instanceof Node\Stmt\ClassMethod) { 233 | yield $stmt; 234 | } 235 | 236 | if (!isset($stmt->stmts)) { 237 | continue; 238 | } 239 | 240 | foreach ($this->iterageStmts($stmt->stmts) as $stmt) { 241 | yield $stmt; 242 | } 243 | } 244 | } 245 | 246 | protected function extractNamespace(array $ast) 247 | { 248 | foreach ($ast as $node) { 249 | if ($node instanceof Node\Stmt\Namespace_) { 250 | return str_replace('\\', '_', (string)$node->name); 251 | } 252 | } 253 | 254 | return 'N' . uniqid('', true); 255 | } 256 | 257 | protected function extractClassImports(array $ast) 258 | { 259 | foreach ($ast as $node) { 260 | if ($node instanceof Node\Stmt\Namespace_) { 261 | foreach ($node->stmts as $stmt) { 262 | if ($stmt instanceof Node\Stmt\Use_) { 263 | yield $stmt; 264 | } 265 | } 266 | } 267 | } 268 | } 269 | 270 | private function locateClassloader() 271 | { 272 | foreach ([ 273 | dirname(__DIR__) . DS . 'vendor' . DS . 'autoload.php', 274 | dirname(dirname(dirname(__DIR__))) . DS . 'autoload.php', 275 | ] as $path) { 276 | if (file_exists($path)) { 277 | return require $path; 278 | } 279 | } 280 | 281 | throw new RuntimeException('Unable to locate class loader'); 282 | } 283 | 284 | /** 285 | * @param ReflectionClass $reflectionClass 286 | * @param string $method 287 | * @param string $class 288 | * @return bool 289 | */ 290 | private function hasMethodAnnotation(ReflectionClass $reflectionClass, $method, $class) 291 | { 292 | $methodReflection = $reflectionClass->getMethod($method); 293 | 294 | return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/AsyncTableInterface.php: -------------------------------------------------------------------------------- 1 | generate($tableName)->getFQCN(); 53 | 54 | $asyncTable = new $asyncTableName(); 55 | 56 | if ($asyncTable instanceof AsyncTableInterface) { 57 | $asyncTable->setUpAsyncTable( 58 | Pool::getInstance(), 59 | $table->getRegistryAlias(), 60 | App::className($tableName, 'Model/Table', 'Table') 61 | ); 62 | } 63 | 64 | static::$tables[$tableName] = $asyncTable; 65 | 66 | return static::$tables[$tableName]; 67 | } 68 | 69 | public static function paginate($tableName, $params, $settings) 70 | { 71 | return Pool::getInstance()->paginate($tableName, $params, $settings); 72 | } 73 | 74 | public static function getInstance() 75 | { 76 | if (null === self::$instance || self::$reset) { 77 | self::$instance = new static(); 78 | self::$reset = false; 79 | } 80 | 81 | return self::$instance; 82 | } 83 | 84 | public static function reset() 85 | { 86 | self::$reset = true; 87 | } 88 | 89 | /** 90 | * @inheritDoc 91 | */ 92 | public function info() 93 | { 94 | return Pool::getInstance()->info(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/GeneratedTable.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 24 | $this->className = $className; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getNamespace() 31 | { 32 | return $this->namespace; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getClassName() 39 | { 40 | return $this->className; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getFQCN() 47 | { 48 | return $this->namespace . '\\' . $this->className; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Pool.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 50 | 51 | Flexible::createFromClass( 52 | WorkerChild::class, 53 | $this->loop, 54 | $this->applyConfig($config) 55 | )->then(function (PoolInterface $pool) { 56 | $this->pool = $pool; 57 | }); 58 | } 59 | 60 | /** 61 | * @param LoopInterface|null $loop 62 | * @param array $config 63 | * @throws \Exception 64 | * @return Pool 65 | */ 66 | public static function getInstance(LoopInterface $loop = null, array $config = []) 67 | { 68 | if (null === self::$instance || self::$reset) { 69 | if (null === $loop) { 70 | throw new \Exception('Missing event loop'); 71 | } 72 | self::$instance = new static($loop, $config); 73 | self::$reset = false; 74 | } 75 | 76 | return self::$instance; 77 | } 78 | 79 | public static function reset() 80 | { 81 | self::$reset = true; 82 | } 83 | 84 | /** 85 | * @param $className 86 | * @param $tableName 87 | * @param $function 88 | * @param array $arguments 89 | * @return PromiseInterface 90 | */ 91 | public function call($className, $tableName, $function, array $arguments) 92 | { 93 | if ($this->pool instanceof PoolInterface) { 94 | return $this->poolCall($className, $tableName, $function, $arguments); 95 | } 96 | 97 | return $this->waitForPoolCall($className, $tableName, $function, $arguments); 98 | } 99 | 100 | public function paginate($tableName, $params, $settings) 101 | { 102 | if ($this->pool instanceof PoolInterface) { 103 | return $this->paginateCall($tableName, $params, $settings); 104 | } 105 | 106 | return $this->waitForPaginateCall($tableName, $params, $settings); 107 | } 108 | 109 | private function paginateCall($tableName, $params, $settings) 110 | { 111 | return $this->pool->rpc(Factory::rpc('paginate', [ 112 | 'table' => $tableName, 113 | 'params' => $params, 114 | 'settings' => $settings, 115 | ])); 116 | } 117 | 118 | protected function waitForPaginateCall($tableName, $params, $settings) 119 | { 120 | $deferred = new Deferred(); 121 | 122 | $this->loop->addPeriodicTimer( 123 | 0.1, 124 | function (TimerInterface $timer) use ($deferred, $tableName, $params, $settings) { 125 | if ($this->pool instanceof PoolInterface) { 126 | $timer->cancel(); 127 | $deferred->resolve($this->paginateCall($tableName, $params, $settings)); 128 | } 129 | } 130 | ); 131 | 132 | return $deferred->promise(); 133 | } 134 | 135 | /** 136 | * @inheritDoc 137 | */ 138 | public function info() 139 | { 140 | if ($this->pool instanceof PoolInterface) { 141 | return $this->pool->info(); 142 | } 143 | 144 | return []; 145 | } 146 | 147 | /** 148 | * @return LoopInterface 149 | */ 150 | public function getLoop() 151 | { 152 | return $this->loop; 153 | } 154 | 155 | /** 156 | * @return PoolInfoInterface 157 | */ 158 | public function getPool() 159 | { 160 | return $this->pool; 161 | } 162 | 163 | /** 164 | * @param array $config 165 | * @return array 166 | */ 167 | protected function applyConfig(array $config) 168 | { 169 | if (!isset($config['processOptions'])) { 170 | $config['processOptions'] = Configure::read('WyriHaximus.React.Cake.Orm.Line'); 171 | } 172 | 173 | if (!isset($config[Options::TTL])) { 174 | $config[Options::TTL] = Configure::read('WyriHaximus.React.Cake.Orm.TTL'); 175 | } 176 | 177 | return $config; 178 | } 179 | 180 | /** 181 | * @param $className 182 | * @param $tableName 183 | * @param $function 184 | * @param array $arguments 185 | * @return PromiseInterface 186 | */ 187 | protected function poolCall($className, $tableName, $function, array $arguments) 188 | { 189 | return $this->pool->rpc(Factory::rpc('table.call', [ 190 | 'className' => $className, 191 | 'function' => $function, 192 | 'table' => $tableName, 193 | 'arguments' => serialize($arguments), 194 | ]))->then(function ($result) { 195 | return \React\Promise\resolve($result['result']); 196 | }); 197 | } 198 | 199 | /** 200 | * @param $tableName 201 | * @param $function 202 | * @param array $arguments 203 | * @return PromiseInterface 204 | */ 205 | protected function waitForPoolCall($tableName, $function, array $arguments) 206 | { 207 | $deferred = new Deferred(); 208 | 209 | $this->loop->addPeriodicTimer( 210 | 0.1, 211 | function (TimerInterface $timer) use ($deferred, $tableName, $function, $arguments) { 212 | if ($this->pool instanceof PoolInterface) { 213 | $timer->cancel(); 214 | $deferred->resolve($this->call($tableName, $function, $arguments)); 215 | } 216 | } 217 | ); 218 | 219 | return $deferred->promise(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Shell/GenerateShell.php: -------------------------------------------------------------------------------- 1 | iteratePath($path); 21 | } 22 | } 23 | 24 | foreach (Plugin::loaded() as $plugin) { 25 | foreach (App::path('Model/Table', $plugin) as $path) { 26 | if (is_dir($path)) { 27 | $this->iteratePath($path); 28 | } 29 | } 30 | } 31 | } 32 | 33 | public function iteratePath($path) 34 | { 35 | foreach ($this->setupIterator($path) as $item) { 36 | $this->iterateClasses($this->getClassByFile(current($item))); 37 | } 38 | } 39 | 40 | public function iterateClasses($classes) 41 | { 42 | foreach ($classes as $class) { 43 | $className = $class->getName(); 44 | (new AsyncTableGenerator( 45 | Configure::read('WyriHaximus.React.Cake.Orm.Cache.AsyncTables') 46 | ))->generate($className, true); 47 | } 48 | } 49 | 50 | public function getClassByFile($fileName) 51 | { 52 | return (new ClassReflector(new SingleFileSourceLocator($fileName, (new BetterReflection())->astLocator())))->getAllClasses(); 53 | } 54 | 55 | /** 56 | * Set options for this console. 57 | * 58 | * @return \Cake\Console\ConsoleOptionParser 59 | */ 60 | // @codingStandardsIgnoreStart 61 | public function getOptionParser() 62 | { 63 | // @codingStandardsIgnoreEnd 64 | return parent::getOptionParser()->addSubcommand( 65 | 'all', 66 | [ 67 | 'short' => 'a', 68 | // @codingStandardsIgnoreStart 69 | 'help' => __('Searches and pregenerates all async tables it finds.'), 70 | // @codingStandardsIgnoreEnd 71 | ] 72 | // @codingStandardsIgnoreStart 73 | )->description(__('Async table pregenerator')); 74 | // @codingStandardsIgnoreEnd 75 | } 76 | 77 | protected function setupIterator($path) 78 | { 79 | return new \RegexIterator(new \RecursiveIteratorIterator( 80 | new \RecursiveDirectoryIterator( 81 | $path, 82 | \FilesystemIterator::KEY_AS_PATHNAME | 83 | \FilesystemIterator::CURRENT_AS_FILEINFO | 84 | \FilesystemIterator::SKIP_DOTS 85 | ), 86 | \RecursiveIteratorIterator::CHILD_FIRST, 87 | \RecursiveIteratorIterator::CATCH_GET_CHILD 88 | ), '/.*?.php$/', \RegexIterator::GET_MATCH); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/TableRegistryTrait.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getTable() 30 | { 31 | return call_user_func_array($this->registry . '::get', func_get_args()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WorkerChild.php: -------------------------------------------------------------------------------- 1 | messenger = $messenger; 31 | $this->loop = $loop; 32 | 33 | $this->messenger->registerRpc('table.call', function (Payload $payload) { 34 | $deferred = new Deferred(); 35 | $this->loop->futureTick(function () use ($payload, $deferred) { 36 | $this->handleTableCall($payload, $deferred); 37 | }); 38 | 39 | return $deferred->promise(); 40 | }); 41 | 42 | $this->messenger->registerRpc('paginate', function (Payload $payload) { 43 | return futurePromise($this->loop, $payload)->then(function ($payload) { 44 | return $this->handlePaginateCall($payload); 45 | }); 46 | }); 47 | } 48 | 49 | /** 50 | * @inheritDoc 51 | */ 52 | public static function create(Messenger $messenger, LoopInterface $loop) 53 | { 54 | require dirname(dirname(dirname(dirname(__DIR__)))) . '/config/paths.php'; 55 | require CORE_PATH . 'config' . DS . 'bootstrap.php'; 56 | Configure::config('default', new Configure\Engine\PhpConfig()); 57 | Configure::load('app', 'default', false); 58 | Cache::setConfig(Configure::consume('Cache')); 59 | ConnectionManager::setConfig(Configure::consume('Datasources')); 60 | 61 | return new self($messenger, $loop); 62 | } 63 | 64 | /** 65 | * @param Payload $payload 66 | * @param Deferred $deferred 67 | */ 68 | protected function handleTableCall(Payload $payload, Deferred $deferred) 69 | { 70 | $result = call_user_func_array([ 71 | TableRegistry::get( 72 | $payload['table']/*, 73 | [ 74 | 'className' => $payload['className'], 75 | 'table' => $payload['table'], 76 | ]*/ 77 | ), 78 | $payload['function'], 79 | ], unserialize($payload['arguments'])); 80 | 81 | if (!($result instanceof Query)) { 82 | $deferred->resolve([ 83 | 'result' => serialize($result), 84 | ]); 85 | 86 | return; 87 | } 88 | 89 | foreach ($result->all() as $row) { 90 | $deferred->notify([ 91 | 'row' => $row, 92 | ]); 93 | } 94 | 95 | $deferred->resolve(); 96 | } 97 | 98 | /** 99 | * @param Payload $payload 100 | * @param Deferred $deferred 101 | */ 102 | protected function handlePaginateCall(Payload $payload) 103 | { 104 | $object = TableRegistry::get( 105 | $payload['table']/*, 106 | [ 107 | 'className' => $payload['className'], 108 | 'table' => $payload['table'], 109 | ]*/ 110 | ); 111 | $paginator = new Paginator(); 112 | 113 | try { 114 | $items = $paginator->paginate($object, $payload['params'], $payload['settings'])->toArray(); 115 | $eos = false; 116 | } catch (PageOutOfBoundsException $pageOutOfBoundsException) { 117 | $items = []; 118 | $eos = true; 119 | } 120 | 121 | return resolve([ 122 | 'items' => $items, 123 | 'eos' => $eos, 124 | 'pagingParams' => $paginator->getPagingParams(), 125 | ]); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test_app/ExpectedGeneratedAsyncTable/C17a66dcf052f6878c3f1c553db4d6bd0_Ff47f6f78cf1b377de64788b3705cda9c.php: -------------------------------------------------------------------------------- 1 | callAsyncOrSync('save', [$entity, $options]); 17 | } 18 | 19 | public function fetchNewest() 20 | { 21 | return $this->callAsyncOrSync('fetchNewest', []); 22 | } 23 | 24 | public function getByMd5($md5) 25 | { 26 | return $this->callAsyncOrSync('getByMd5', [$md5]); 27 | } 28 | 29 | public function getByEntity($md5) 30 | { 31 | return $this->callAsyncOrSync('getByEntity', [$md5]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test_app/Table/ScreenshotsTable.php: -------------------------------------------------------------------------------- 1 | find()->orderDesc('id')->firstOrFail(); 16 | } 17 | 18 | public function getByMd5($md5) 19 | { 20 | return $this->findByMd5($md5); 21 | } 22 | 23 | public function getByEntity($md5) 24 | { 25 | return $this->findByMd5($md5); 26 | } 27 | } 28 | --------------------------------------------------------------------------------