├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── cycle.php ├── phpunit.xml ├── src ├── Auth │ └── UserProvider.php ├── Commands │ ├── Migrate.php │ └── RefreshSchema.php ├── Contracts │ ├── EntityManager.php │ └── SchemaManager.php ├── Database │ ├── Connection.php │ ├── Factory.php │ └── FactoryBuilder.php ├── Entity │ ├── Manager.php │ └── Relations │ │ └── MaterializerManager.php ├── Facades │ ├── DatabaseManager.php │ ├── EntityManager.php │ ├── ORM.php │ └── Transaction.php ├── Mapper.php ├── Migrations │ └── FileRepository.php ├── Paginator.php ├── Providers │ └── LaravelServiceProvider.php ├── Repository.php └── SchemaManager.php └── tests ├── PaginatorTest.php └── TestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 -------------------------------------------------------------------------------- /.gitattributes : -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pavel Buchnev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cycle ORM integration for the Laravel Framework 2 | 3 | ![laracycle](https://user-images.githubusercontent.com/773481/86586434-8c675700-bf90-11ea-836c-59b7485f6a8f.png) 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/butschster/cycle-orm/v/stable)](https://packagist.org/packages/butschster/cycle-orm) [![Total Downloads](https://poser.pugx.org/butschster/cycle-orm/downloads)](https://packagist.org/packages/butschster/cycle-orm) [![License](https://poser.pugx.org/butschster/cycle-orm/license)](https://packagist.org/packages/butschster/cycle-orm) 6 | 7 | Cycle is a PHP DataMapper ORM and Data Modelling engine designed to safely work in classic and daemonized PHP 8 | applications (like RoadRunner). The ORM provides flexible configuration options to model datasets, a powerful query 9 | builder, and supports dynamic mapping schemas. The engine can work with plain PHP objects, support annotation 10 | declarations, and proxies via extensions. 11 | 12 | Full information - https://cycle-orm.dev/docs 13 | 14 | ### Requirements 15 | - Laravel 7.x 16 | - PHP 7.4 and above 17 | 18 | ## Installation and Configuration 19 | 20 | From the command line run 21 | ```shell script 22 | composer require butschster/cycle-orm 23 | ``` 24 | 25 | Optionally you can register the EntityManager, Transaction and/or ORM facade: 26 | ```php 27 | 'DatabaseManager' => Butschster\Cycle\Facades\DatabaseManager::class, 28 | 'Transaction' => Butschster\Cycle\Facades\Transaction::class, 29 | 'ORM' => Butschster\Cycle\Facades\ORM::class, 30 | 'EntityManager' => Butschster\Cycle\Facades\EntityManager::class, 31 | ``` 32 | 33 | ### Env variables 34 | 35 | ``` 36 | DB_CONNECTION=postgres 37 | DB_HOST=127.0.0.1 38 | DB_PORT= # Default port 5432 39 | DB_DATABASE=homestead 40 | DB_USERNAME=root 41 | DB_PASSWORD= 42 | 43 | DB_MIGRATIONS_TABLE=migrations # Migrations table name 44 | 45 | DB_SCHEMA_SYNC=false # Sync DB schema without migrations 46 | DB_SCHEMA_CACHE=true # Cache DB schema 47 | DB_SCHEMA_CACHE_DRIVER=file # DB schema cache driver 48 | ``` 49 | 50 | ### Configuration 51 | 52 | Publish the config file. 53 | ```shell script 54 | php artisan vendor:publish --provider="Butschster\Cycle\Providers\LaravelServiceProvider" --tag=config 55 | ``` 56 | 57 | #### Configure Databases 58 | The list of available connections and databases can be listed in the `config/cycle.php` - `database` section. 59 | For more information see https://cycle-orm.dev/docs/basic-connect#configure-databases 60 | 61 | #### Getting Database Manager ($dbal) 62 | `DatabaseManager` registered as a singleton container 63 | 64 | ```php 65 | $dbal = $this->app->get(\Spiral\Database\DatabaseManager::class); 66 | // Or 67 | $dbal = $this->app->get(\Spiral\Database\DatabaseProviderInterface::class); 68 | ``` 69 | 70 | 71 | **That's it!** 72 | 73 | ### Console commands 74 | 75 | #### php artisan cycle:migrate 76 | Run cycle orm migrations from the directory. 77 | 78 | #### php artisan cycle:refresh 79 | Refresh database schema. 80 | 81 | ## Usage 82 | By default, class locator looks for entities in app folder. You can specify locations in `config/cycle.php` config file. 83 | 84 | ```php 85 | ... 86 | 'directories' => [ 87 | app_path(), 88 | ], 89 | ... 90 | ``` 91 | 92 | ### Entity Manager 93 | The EntityManager is the central access point to ORM functionality. It can be used to find, persist and remove entities. 94 | 95 | #### Using the EntityManager 96 | You can use the facade, container or Dependency injection to access the EntityManager methods 97 | ```php 98 | EntityManager::persist($entity); 99 | 100 | // Or 101 | 102 | app(\Butschster\Cycle\Contracts\EntityManager::class)->persist($entity); 103 | 104 | // Or 105 | 106 | use Butschster\Cycle\Contracts\EntityManager; 107 | 108 | class ExampleController extends Controller 109 | { 110 | protected $em; 111 | 112 | public function __construct(EntityManager $em) 113 | { 114 | $this->em = $em; 115 | } 116 | } 117 | ``` 118 | 119 | #### Finding entities 120 | Entities are objects with identity. Their identity has a conceptual meaning inside your domain. In a CMS application 121 | each article has a unique id. You can uniquely identify each article by that id. 122 | 123 | ```php 124 | $article = EntityManager::findByPK('App\Article', 1); 125 | $article->setTitle('Different title'); 126 | 127 | $article2 = EntityManager::findByPK('App\Article', 1); 128 | 129 | if ($article === $article2) { 130 | echo "Yes we are the same!"; 131 | } 132 | ``` 133 | 134 | #### Persisting 135 | By passing the entity through the persist method of the EntityManager, that entity becomes MANAGED, which means that 136 | its persistence is from now on managed by an EntityManager. 137 | 138 | ```php 139 | $article = new Article; 140 | $article->setTitle('Let\'s learn about persisting'); 141 | 142 | EntityManager::persist($article); 143 | ``` 144 | 145 | #### Deleting 146 | An entity can be deleted from persistent storage by passing it to the delete($entity) method. 147 | ```php 148 | EntityManager::delete($article); 149 | ``` 150 | 151 | ### Example 152 | 153 | #### User 154 | ```php 155 | id; 180 | } 181 | 182 | /** 183 | * @return string 184 | */ 185 | public function getAuthIdentifierName(): string 186 | { 187 | return 'id'; 188 | } 189 | 190 | /** 191 | * @return string 192 | */ 193 | public function getAuthIdentifier(): string 194 | { 195 | return $this->getId(); 196 | } 197 | 198 | /** 199 | * @return string 200 | */ 201 | public function getAuthPassword(): string 202 | { 203 | return $this->password; 204 | } 205 | 206 | /** 207 | * @param string $password 208 | */ 209 | public function setAuthPassword(string $password): void 210 | { 211 | $this->password = $password; 212 | } 213 | 214 | /** 215 | * @return string 216 | */ 217 | public function getRememberToken(): string 218 | { 219 | return $this->rememberToken; 220 | } 221 | 222 | /** 223 | * @param string $value 224 | */ 225 | public function setRememberToken($value): void 226 | { 227 | $this->rememberToken = $value; 228 | } 229 | 230 | /** 231 | * @return string 232 | */ 233 | public function getRememberTokenName(): string 234 | { 235 | return 'rememberToken'; 236 | } 237 | } 238 | ``` 239 | 240 | #### Repository 241 | ```php 242 | namespace App; 243 | 244 | use Butschster\Cycle\Repository; 245 | 246 | class UserRepository extends Repository 247 | { 248 | /** @inheritDoc */ 249 | public function findByUsername(string $username): ?User 250 | { 251 | return $this->findOne(['username' => $username]); 252 | } 253 | } 254 | ``` 255 | 256 | #### Create user 257 | ```php 258 | use Cycle\ORM\ORMInterface; 259 | use Butschster\Cycle\Facades\ORM; 260 | use Butschster\Cycle\Facades\EntityManager; 261 | 262 | $user = new User(); 263 | 264 | $repository = app(ORMInterface::class)->getRepository($user); 265 | // or 266 | $repository = ORM::getRepository($user); 267 | // or 268 | $repository = ORM::getRepository(User::class); 269 | 270 | $repository->persist($user); 271 | 272 | // or 273 | 274 | EntityManager::persist($user); 275 | ``` 276 | 277 | #### Update user 278 | ```php 279 | use Butschster\Cycle\Facades\ORM; 280 | use Butschster\Cycle\Facades\EntityManager; 281 | 282 | $repository = ORM::getRepository(User::class); 283 | $user = $repository->findByPK('5c9e177b0a975a6eeccf5960'); 284 | $user->setAuthPassword('secret'); 285 | 286 | $repository->persist($user); 287 | 288 | // or 289 | 290 | EntityManager::persist($user); 291 | ``` 292 | 293 | #### Delete user 294 | ```php 295 | use Butschster\Cycle\Facades\ORM; 296 | use Butschster\Cycle\Facades\EntityManager; 297 | 298 | $repository = ORM::getRepository(User::class); 299 | $user = $repository->findByPK('5c9e177b0a975a6eeccf5960'); 300 | 301 | $repository->delete($user); 302 | 303 | // or 304 | 305 | EntityManager::delete($user); 306 | ``` 307 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "butschster/cycle-orm", 3 | "description": "Cycle ORM integration for the Laravel Framework", 4 | "keywords": [ 5 | "laravel", 6 | "lumen", 7 | "orm", 8 | "database", 9 | "cycle" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Pavel Buchnev", 15 | "email": "butschster@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.4|^8.0", 20 | "illuminate/config": "^7.0|^8.0", 21 | "illuminate/database": "^7.0|^8.0", 22 | "illuminate/support": "^7.0|^8.0", 23 | "illuminate/contracts": "^7.0|^8.0", 24 | "illuminate/console": "^7.0|^8.0", 25 | "illuminate/pagination": "^7.0|^8.0", 26 | "cycle/annotated": "^2.0", 27 | "cycle/migrations": "^1.0", 28 | "cycle/orm": "^1.2", 29 | "cycle/proxy-factory": "^1.2.1", 30 | "cycle/schema-builder": "^1.1" 31 | }, 32 | "require-dev": { 33 | "fzaninotto/faker": "^1.9.1", 34 | "mockery/mockery": "^1.0", 35 | "phpunit/phpunit": "^7.0|^8.0|^9.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Butschster\\Cycle\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Butschster\\Tests\\": "tests/" 45 | } 46 | }, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "Butschster\\Cycle\\Providers\\LaravelServiceProvider" 51 | ] 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true 56 | } 57 | -------------------------------------------------------------------------------- /config/cycle.php: -------------------------------------------------------------------------------- 1 | [ 7 | app_path(), 8 | ], 9 | 10 | 'database' => [ 11 | 'default' => 'default', 12 | 13 | 'databases' => [ 14 | 'default' => [ 15 | 'connection' => env('DB_CONNECTION', 'postgres'), 16 | ], 17 | ], 18 | 19 | 'connections' => [ 20 | 'sqlite' => [ 21 | 'driver' => Spiral\Database\Driver\SQLite\SQLiteDriver::class, 22 | 'options' => [ 23 | 'connection' => sprintf( 24 | 'sqlite:%s', 25 | env('DB_DATABASE', database_path('database.sqlite')) 26 | ), 27 | 'username' => env('DB_USERNAME', 'root'), 28 | 'password' => env('DB_PASSWORD'), 29 | ] 30 | ], 31 | 32 | 'mysql' => [ 33 | 'driver' => Spiral\Database\Driver\MySQL\MySQLDriver::class, 34 | 'options' => [ 35 | 'connection' => sprintf( 36 | 'mysql:host=%s;port=%d;dbname=%s', 37 | env('DB_HOST', '127.0.0.1'), 38 | env('DB_PORT', 3304), 39 | env('DB_DATABASE', 'homestead') 40 | ), 41 | 'username' => env('DB_USERNAME', 'root'), 42 | 'password' => env('DB_PASSWORD'), 43 | ] 44 | ], 45 | 46 | 'postgres' => [ 47 | 'driver' => Spiral\Database\Driver\Postgres\PostgresDriver::class, 48 | 'options' => [ 49 | 'connection' => sprintf( 50 | 'pgsql:host=%s;port=%d;dbname=%s;', 51 | env('DB_HOST', '127.0.0.1'), 52 | env('DB_PORT', 5432), 53 | env('DB_DATABASE', 'homestead') 54 | ), 55 | 'username' => env('DB_USERNAME', 'root'), 56 | 'password' => env('DB_PASSWORD'), 57 | ], 58 | ], 59 | ], 60 | ], 61 | 62 | 'schema' => [ 63 | // Sync db schema with database without migrations 64 | 'sync' => env('DB_SCHEMA_SYNC', false), 65 | 66 | // Cache schema 67 | // Кеширование схемы. После изменение сущности необходимо будет сбрасывать схему 68 | 'cache' => [ 69 | 'storage' => env('DB_SCHEMA_CACHE_DRIVER', 'file'), 70 | 'enabled' => (bool)env('DB_SCHEMA_CACHE', true), 71 | ], 72 | 73 | 'defaults' => [ 74 | Schema::MAPPER => Butschster\Cycle\Mapper::class, 75 | Schema::REPOSITORY => Butschster\Cycle\Repository::class, 76 | Schema::SOURCE => Cycle\ORM\Select\Source::class, 77 | ], 78 | ], 79 | 80 | 'migrations' => [ 81 | 'directory' => database_path('migrations/cycle/'), 82 | 'table' => env('DB_MIGRATIONS_TABLE', 'migrations'), 83 | ], 84 | 85 | // https://cycle-orm.dev/docs/advanced-promise#proxies-and-promises 86 | 'relations' => [ 87 | 'materializer' => [ 88 | 'driver' => env('DB_MATERIALIZER_DRIVER', 'eval'), 89 | 'drivers' => [ 90 | 'file' => [ 91 | 'path' => storage_path('framework/cache/entities'), 92 | ] 93 | ] 94 | ], 95 | ], 96 | ]; 97 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Auth/UserProvider.php: -------------------------------------------------------------------------------- 1 | orm = $orm; 25 | $this->model = $model; 26 | $this->hasher = $hasher; 27 | } 28 | 29 | /** @inheritDoc */ 30 | public function retrieveById($identifier) 31 | { 32 | return $this->getRepository()->findByPK($identifier); 33 | } 34 | 35 | /** @inheritDoc */ 36 | public function retrieveByToken($identifier, $token) 37 | { 38 | $modelInstance = $this->getAuthenticatableInstance(); 39 | 40 | return $this->getRepository()->findOne([ 41 | $modelInstance->getAuthIdentifierName() => $identifier, 42 | $modelInstance->getRememberTokenName() => $token, 43 | ]); 44 | } 45 | 46 | /** @inheritDoc */ 47 | public function updateRememberToken(Authenticatable $user, $token) 48 | { 49 | $user->setRememberToken($token); 50 | $this->getRepository()->persist($user); 51 | } 52 | 53 | /** @inheritDoc */ 54 | public function retrieveByCredentials(array $credentials) 55 | { 56 | $criteria = []; 57 | foreach ($credentials as $key => $value) { 58 | if (!Str::contains($key, 'password')) { 59 | $criteria[$key] = $value; 60 | } 61 | } 62 | 63 | return $this->getRepository()->findOne($criteria); 64 | } 65 | 66 | /** @inheritDoc */ 67 | public function validateCredentials(Authenticatable $user, array $credentials) 68 | { 69 | return $this->hasher->check($credentials['password'], $user->getAuthPassword()); 70 | } 71 | 72 | /** 73 | * Returns instantiated entity. 74 | * 75 | * @return Authenticatable 76 | * @throws ReflectionException 77 | */ 78 | protected function getAuthenticatableInstance(): Authenticatable 79 | { 80 | $refEntity = new ReflectionClass($this->model); 81 | 82 | return $refEntity->newInstanceWithoutConstructor(); 83 | } 84 | 85 | /** 86 | * @return RepositoryInterface 87 | */ 88 | protected function getRepository(): RepositoryInterface 89 | { 90 | return $this->orm->getRepository($this->model); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Commands/Migrate.php: -------------------------------------------------------------------------------- 1 | isConfigured()) { 25 | $migrator->configure(); 26 | } 27 | 28 | while (($migration = $migrator->run()) !== null) { 29 | $this->info('Migrate ' . $migration->getState()->getName()); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Commands/RefreshSchema.php: -------------------------------------------------------------------------------- 1 | call('db:wipe'); 25 | 26 | $schemaManager->flushSchemaData(); 27 | 28 | $this->info('Database schema cache flushed.'); 29 | 30 | if ($schemaManager->isSyncMode()) { 31 | $schemaManager->createSchema(); 32 | } else { 33 | $this->call('cycle:migrate'); 34 | $schemaManager->createSchema(); 35 | $this->call('cycle:migrate'); 36 | } 37 | 38 | $this->info('Database schema updated.'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Contracts/EntityManager.php: -------------------------------------------------------------------------------- 1 | ORM = $ORM; 29 | $this->driver = $ORM->getFactory()->database()->getDriver(); 30 | $this->connection = $connection; 31 | } 32 | 33 | /** @inheritDoc */ 34 | public function query(): BuilderInterface 35 | { 36 | return $this->driver->getQueryBuilder(); 37 | } 38 | 39 | /** @inheritDoc */ 40 | public function table($table, $as = null) 41 | { 42 | return $this->query()->selectQuery('')->from($table); 43 | } 44 | 45 | /** @inheritDoc */ 46 | public function selectOne($query, $bindings = [], $useReadPdo = true) 47 | { 48 | $records = $this->select($query, $bindings, $useReadPdo); 49 | 50 | return array_shift($records); 51 | } 52 | 53 | /** @inheritDoc */ 54 | public function select($query, $bindings = [], $useReadPdo = true) 55 | { 56 | return $this->statement($query, $bindings)->fetchAll(); 57 | } 58 | 59 | /** @inheritDoc */ 60 | public function cursor($query, $bindings = [], $useReadPdo = true) 61 | { 62 | $statement = $this->statement($query, $bindings); 63 | 64 | while ($record = $statement->fetch()) { 65 | yield $record; 66 | } 67 | } 68 | 69 | /** @inheritDoc */ 70 | public function insert($query, $bindings = []) 71 | { 72 | return $this->statement($query, $bindings); 73 | } 74 | 75 | /** @inheritDoc */ 76 | public function update($query, $bindings = []) 77 | { 78 | return $this->affectingStatement($query, $bindings); 79 | } 80 | 81 | /** @inheritDoc */ 82 | public function delete($query, $bindings = []) 83 | { 84 | return $this->affectingStatement($query, $bindings); 85 | } 86 | 87 | /** @inheritDoc */ 88 | public function statement($query, $bindings = []) 89 | { 90 | return $this->driver->query($query, $bindings); 91 | } 92 | 93 | /** @inheritDoc */ 94 | public function affectingStatement($query, $bindings = []) 95 | { 96 | return $this->driver->execute($query, $bindings); 97 | } 98 | 99 | /** @inheritDoc */ 100 | public function unprepared($query) 101 | { 102 | return $this->driver->execute($query); 103 | } 104 | 105 | /** @inheritDoc */ 106 | public function prepareBindings(array $bindings) 107 | { 108 | return $this->connection->prepareBindings($bindings); 109 | } 110 | 111 | /** @inheritDoc */ 112 | public function pretend(Closure $callback) 113 | { 114 | return $this->connection->pretend($callback); 115 | } 116 | 117 | /** @inheritDoc */ 118 | public function transaction(Closure $callback, $attempts = 1) 119 | { 120 | for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) { 121 | $this->beginTransaction(); 122 | 123 | // We'll simply execute the given callback within a try / catch block and if we 124 | // catch any exception we can rollback this transaction so that none of this 125 | // gets actually persisted to a database or stored in a permanent fashion. 126 | try { 127 | $callbackResult = $callback($this); 128 | } 129 | 130 | // If we catch an exception we'll rollback this transaction and try again if we 131 | // are not out of attempts. If we are out of attempts we will just throw the 132 | // exception back out and let the developer handle an uncaught exceptions. 133 | catch (Throwable $e) { 134 | $this->handleTransactionException( 135 | $e, 136 | $currentAttempt, 137 | $attempts 138 | ); 139 | 140 | continue; 141 | } 142 | 143 | try { 144 | $this->commit(); 145 | } catch (Throwable $e) { 146 | $this->handleCommitTransactionException( 147 | $e, 148 | $currentAttempt, 149 | $attempts 150 | ); 151 | 152 | continue; 153 | } 154 | 155 | return $callbackResult; 156 | } 157 | } 158 | 159 | /** @inheritDoc */ 160 | public function beginTransaction() 161 | { 162 | $this->driver->beginTransaction(); 163 | } 164 | 165 | /** @inheritDoc */ 166 | public function commit() 167 | { 168 | $this->driver->commitTransaction(); 169 | } 170 | 171 | /** @inheritDoc */ 172 | public function rollBack($toLevel = null) 173 | { 174 | $this->driver->rollbackTransaction(); 175 | } 176 | 177 | /** @inheritDoc */ 178 | public function transactionLevel() 179 | { 180 | return 0; 181 | } 182 | 183 | /** 184 | * Handle an exception encountered when running a transacted statement. 185 | * 186 | * @param Throwable $e 187 | * @param int $currentAttempt 188 | * @param int $maxAttempts 189 | * @return void 190 | * 191 | * @throws Throwable 192 | */ 193 | protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts) 194 | { 195 | // On a deadlock, MySQL rolls back the entire transaction so we can't just 196 | // retry the query. We have to throw this exception all the way out and 197 | // let the developer handle it in another way. We will decrement too. 198 | if ($this->causedByConcurrencyError($e)) { 199 | throw $e; 200 | } 201 | 202 | // If there was an exception we will rollback this transaction and then we 203 | // can check if we have exceeded the maximum attempt count for this and 204 | // if we haven't we will return and try this query again in our loop. 205 | $this->rollBack(); 206 | 207 | if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) { 208 | return; 209 | } 210 | 211 | throw $e; 212 | } 213 | 214 | /** 215 | * Handle an exception encountered when committing a transaction. 216 | * 217 | * @param Throwable $e 218 | * @param int $currentAttempt 219 | * @param int $maxAttempts 220 | * @return void 221 | * 222 | * @throws Throwable 223 | */ 224 | protected function handleCommitTransactionException(Throwable $e, $currentAttempt, $maxAttempts) 225 | { 226 | if ($this->causedByConcurrencyError($e) && $currentAttempt < $maxAttempts) { 227 | return; 228 | } 229 | 230 | throw $e; 231 | } 232 | 233 | public function raw($value) 234 | { 235 | return new Fragment($value); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Database/Factory.php: -------------------------------------------------------------------------------- 1 | orm = $orm; 23 | } 24 | 25 | /** 26 | * Create a builder for the given model. 27 | * 28 | * @param string $class 29 | * @return FactoryBuilder 30 | */ 31 | public function of($class): FactoryBuilder 32 | { 33 | return new FactoryBuilder( 34 | $class, 35 | $this->definitions, 36 | $this->states, 37 | $this->afterMaking, 38 | $this->afterCreating, 39 | $this->orm, 40 | $this->faker 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Database/FactoryBuilder.php: -------------------------------------------------------------------------------- 1 | orm = $orm; 34 | } 35 | 36 | /** 37 | * Create a collection of models and persist them to the database. 38 | * 39 | * @param array $attributes 40 | * @return Collection|mixed 41 | */ 42 | public function create(array $attributes = []) 43 | { 44 | $results = $this->make($attributes, true); 45 | 46 | if ($results instanceof Collection) { 47 | $this->store($results); 48 | 49 | $this->callAfterCreating($results); 50 | } else { 51 | $this->store(collect([$results])); 52 | 53 | $this->callAfterCreating(collect([$results])); 54 | } 55 | 56 | return $results; 57 | } 58 | 59 | /** 60 | * Create a collection of models and persist them to the database. 61 | * 62 | * @param iterable $records 63 | * @return Collection|mixed 64 | */ 65 | public function createMany(iterable $records) 66 | { 67 | return collect($records)->map(function ($attributes) { 68 | return $this->create($attributes); 69 | }); 70 | } 71 | 72 | /** 73 | * Create a collection of models. 74 | * 75 | * @param array $attributes 76 | * @param bool $persist 77 | * @return Collection|mixed 78 | * @throws ReflectionException 79 | */ 80 | public function make(array $attributes = [], bool $persist = false) 81 | { 82 | if ($this->amount === null) { 83 | return tap($this->makeInstance($attributes, $persist), function ($instance) { 84 | $this->callAfterMaking(collect([$instance])); 85 | }); 86 | } 87 | 88 | if ($this->amount < 1) { 89 | return collect(); 90 | } 91 | 92 | $instances = collect(range(1, $this->amount))->map(function () use ($attributes, $persist) { 93 | return $this->makeInstance($attributes, $persist); 94 | }); 95 | 96 | $this->callAfterMaking($instances); 97 | 98 | return $instances; 99 | } 100 | 101 | /** 102 | * Set the connection name on the results and store them. 103 | * 104 | * @param Collection $results 105 | * @return void 106 | */ 107 | protected function store($results) 108 | { 109 | $results->map(function ($entity) { 110 | $repository = $this->orm->getRepository($this->class); 111 | 112 | $repository->persist($entity); 113 | 114 | return $entity; 115 | }); 116 | } 117 | 118 | /** 119 | * Make an instance of the model with the given attributes. 120 | * 121 | * @param array $attributes 122 | * @param bool $persist 123 | * @return object 124 | * @throws ReflectionException 125 | */ 126 | protected function makeInstance(array $attributes = [], bool $persist = false) 127 | { 128 | $hydrator = new ReflectionHydrator(); 129 | $object = (new ReflectionClass($this->class))->newInstanceWithoutConstructor(); 130 | 131 | return $hydrator->hydrate( 132 | $this->getRawAttributes($attributes, $persist), 133 | $object 134 | ); 135 | } 136 | 137 | /** 138 | * Get a raw attributes array for the model. 139 | * 140 | * @param array $attributes 141 | * @param bool $persist 142 | * @return mixed 143 | * 144 | */ 145 | protected function getRawAttributes(array $attributes = [], bool $persist = false) 146 | { 147 | if (!isset($this->definitions[$this->class])) { 148 | throw new InvalidArgumentException("Unable to locate factory for [{$this->class}]."); 149 | } 150 | 151 | $definition = call_user_func( 152 | $this->definitions[$this->class], 153 | $this->faker, 154 | $attributes 155 | ); 156 | 157 | return $this->expandAttributes( 158 | array_merge($this->applyStates($definition, $attributes), $attributes), 159 | $persist 160 | ); 161 | } 162 | 163 | /** 164 | * Expand all attributes to their underlying values. 165 | * 166 | * @param array $attributes 167 | * @param bool $persist 168 | * @return array 169 | */ 170 | protected function expandAttributes(array $attributes, bool $persist = false) 171 | { 172 | foreach ($attributes as &$attribute) { 173 | if (is_callable($attribute) && !is_string($attribute) && !is_array($attribute)) { 174 | $attribute = $attribute($attributes); 175 | } 176 | 177 | if ($attribute instanceof static) { 178 | if ($persist) { 179 | $attribute = $attribute->create(); 180 | } else { 181 | $attribute = $attribute->make(); 182 | } 183 | } 184 | } 185 | 186 | return $attributes; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Entity/Manager.php: -------------------------------------------------------------------------------- 1 | orm = $orm; 16 | } 17 | 18 | public function findByPK(string $entity, $id) 19 | { 20 | return $this->orm->getRepository($entity) 21 | ->findByPK($id); 22 | } 23 | 24 | public function findOne(string $entity, array $scope = []) 25 | { 26 | return $this->orm->getRepository($entity) 27 | ->findOne($scope); 28 | } 29 | 30 | public function findAll(string $entity, array $scope = [], array $orderBy = []): Collection 31 | { 32 | return $this->orm->getRepository($entity) 33 | ->findAll($scope, $orderBy); 34 | } 35 | 36 | public function persist(object $entity, bool $cascade = true, bool $run = true): void 37 | { 38 | $this->orm->getRepository($entity) 39 | ->persist($entity, $cascade, $run); 40 | } 41 | 42 | public function delete(object $entity, bool $cascade = true, bool $run = true): void 43 | { 44 | $this->orm->getRepository($entity) 45 | ->delete($entity, $cascade, $run); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Entity/Relations/MaterializerManager.php: -------------------------------------------------------------------------------- 1 | config->get('cycle.relations.materializer.driver') ?? 'eval'; 16 | } 17 | 18 | protected function createEvalDriver(): MaterializerInterface 19 | { 20 | return new EvalMaterializer(); 21 | } 22 | 23 | protected function createFileDriver(): MaterializerInterface 24 | { 25 | return new FileMaterializer( 26 | new ModificationInspector(), 27 | $this->config->get('cycle.relations.materializer.drivers.file.path') 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Facades/DatabaseManager.php: -------------------------------------------------------------------------------- 1 | config = $config; 51 | $this->files = new Files(); 52 | $this->factory = $factory ?? new Container(); 53 | $this->inflector = (new \Doctrine\Inflector\Rules\English\InflectorFactory())->build(); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getMigrations(): array 60 | { 61 | $migrations = []; 62 | 63 | foreach ($this->getFiles() as $f) { 64 | if (!class_exists($f['class'], false)) { 65 | //Attempting to load migration class (we can not relay on autoloading here) 66 | require_once($f['filename']); 67 | } 68 | 69 | /** @var MigrationInterface $migration */ 70 | $migration = $this->factory->make($f['class']); 71 | 72 | $migrations[$f['created']->getTimestamp() . $f['chunk']] = $migration->withState( 73 | new State($f['name'], $f['created']) 74 | ); 75 | } 76 | 77 | ksort($migrations); 78 | 79 | return $migrations; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function registerMigration(string $name, string $class, string $body = null): string 86 | { 87 | if (empty($body) && !class_exists($class)) { 88 | throw new RepositoryException( 89 | "Unable to register migration '{$class}', representing class does not exists" 90 | ); 91 | } 92 | 93 | $inflectedName = $this->inflector->tableize($name); 94 | 95 | foreach ($this->getMigrations() as $migration) { 96 | if (get_class($migration) === $class) { 97 | throw new RepositoryException( 98 | "Unable to register migration '{$class}', migration already exists" 99 | ); 100 | } 101 | 102 | if ($migration->getState()->getName() === $inflectedName) { 103 | throw new RepositoryException( 104 | "Unable to register migration '{$inflectedName}', migration under the same name already exists" 105 | ); 106 | } 107 | } 108 | 109 | if (empty($body)) { 110 | //Let's read body from a given class filename 111 | $body = $this->files->read((new \ReflectionClass($class))->getFileName()); 112 | } 113 | 114 | $filename = $this->createFilename($name); 115 | 116 | //Copying 117 | $this->files->write($filename, $body, FilesInterface::READONLY, true); 118 | 119 | return $filename; 120 | } 121 | 122 | /** 123 | * Internal method to fetch all migration filenames. 124 | */ 125 | private function getFiles(): \Generator 126 | { 127 | foreach ($this->files->getFiles($this->config->getDirectory(), '*.php') as $filename) { 128 | $reflection = new ReflectionFile($filename); 129 | $definition = explode('_', basename($filename)); 130 | 131 | if (count($definition) < 3) { 132 | throw new RepositoryException("Invalid migration filename '{$filename}'"); 133 | } 134 | 135 | yield [ 136 | 'filename' => $filename, 137 | 'class' => $reflection->getClasses()[0], 138 | 'created' => \DateTime::createFromFormat( 139 | self::TIMESTAMP_FORMAT, 140 | $definition[0] 141 | ), 142 | 'chunk' => $definition[1], 143 | 'name' => $definition[0].'_'.str_replace( 144 | '.php', 145 | '', 146 | join('_', array_slice($definition, 2)) 147 | ) 148 | ]; 149 | } 150 | } 151 | 152 | /** 153 | * Request new migration filename based on user input and current timestamp. 154 | * 155 | * @param string $name 156 | * @return string 157 | */ 158 | private function createFilename(string $name): string 159 | { 160 | $name = $this->inflector->tableize($name); 161 | 162 | $filename = sprintf( 163 | self::FILENAME_FORMAT, 164 | date(self::TIMESTAMP_FORMAT), 165 | $this->chunkID++, 166 | $name 167 | ); 168 | 169 | return $this->files->normalizePath( 170 | $this->config->getDirectory() . FilesInterface::SEPARATOR . $filename 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Paginator.php: -------------------------------------------------------------------------------- 1 | paginator = $paginator; 19 | $this->perPage = $paginator->getLimit(); 20 | $this->currentPage = $paginator->getPage(); 21 | $this->pageName = $pageName; 22 | $this->items = $items; 23 | } 24 | 25 | public function total(): int 26 | { 27 | return $this->paginator->count(); 28 | } 29 | 30 | public function lastPage(): int 31 | { 32 | return $this->paginator->countPages(); 33 | } 34 | 35 | public function nextPageUrl(): ?string 36 | { 37 | if ($this->hasMorePages()) { 38 | return $this->url($this->currentPage() + 1); 39 | } 40 | 41 | return null; 42 | } 43 | 44 | public function hasMorePages(): bool 45 | { 46 | return $this->currentPage() < $this->lastPage(); 47 | } 48 | 49 | public function render($view = null, $data = []) 50 | { 51 | // TODO: Implement render() method. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Providers/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->configurationIsCached()) { 48 | $this->mergeConfigFrom(__DIR__ . '/../../config/cycle.php', 'cycle'); 49 | } 50 | 51 | $this->publishes([ 52 | __DIR__ . '/../../config/cycle.php' => config_path('cycle.php'), 53 | ], 'config'); 54 | 55 | $this->registerAuthUserProvider(); 56 | $this->registerDatabaseConnection(); 57 | } 58 | 59 | public function register() 60 | { 61 | AnnotationRegistry::registerLoader('class_exists'); 62 | $this->registerConsoleCommands(); 63 | $this->registerDatabaseConfig(); 64 | $this->registerDatabaseManager(); 65 | $this->registerEntityManager(); 66 | $this->registerDatabaseSchema(); 67 | $this->registerRelationMaterializer(); 68 | $this->registerOrm(); 69 | $this->registerTokenizer(); 70 | $this->registerClassLocator(); 71 | $this->registerMigrationConfig(); 72 | $this->registerMigrator(); 73 | $this->registerDatabaseFactory(); 74 | $this->registerSchemaManager(); 75 | } 76 | 77 | protected function registerConsoleCommands(): void 78 | { 79 | $this->commands([ 80 | Commands\Migrate::class, 81 | Commands\RefreshSchema::class, 82 | ]); 83 | } 84 | 85 | public function registerDatabaseConnection(): void 86 | { 87 | Connection::resolverFor('cycle', function () { 88 | return new DatabaseConnection( 89 | $this->app['db.connection'], 90 | $this->app[ORMInterface::class] 91 | ); 92 | }); 93 | } 94 | 95 | public function registerAuthUserProvider(): void 96 | { 97 | $this->app->make('auth')->provider('cycle', function ($app, $config) { 98 | return new UserProvider( 99 | $app[ORMInterface::class], 100 | $config['model'], 101 | $app['hash'], 102 | ); 103 | }); 104 | } 105 | 106 | protected function registerDatabaseConfig(): void 107 | { 108 | $this->app->singleton(DatabaseConfig::class, function () { 109 | return new DatabaseConfig( 110 | config('cycle.database') 111 | ); 112 | }); 113 | } 114 | 115 | protected function registerDatabaseManager(): void 116 | { 117 | $this->app->singleton(DatabaseProviderInterface::class, function () { 118 | return new DatabaseManager( 119 | $this->app[DatabaseConfig::class] 120 | ); 121 | }); 122 | 123 | $this->app->alias(DatabaseProviderInterface::class, DatabaseManager::class); 124 | } 125 | 126 | private function registerEntityManager() 127 | { 128 | $this->app->singleton(EntityManagerContract::class, function () { 129 | return $this->app[EntityManager::class]; 130 | }); 131 | } 132 | 133 | protected function registerDatabaseSchema(): void 134 | { 135 | $this->app->singleton(SchemaInterface::class, function () { 136 | return $this->app[SchemaManagerContract::class]->createSchema(); 137 | }); 138 | } 139 | 140 | protected function registerRelationMaterializer(): void 141 | { 142 | $this->app->singleton(MaterializerInterface::class, function () { 143 | return (new MaterializerManager($this->app))->driver(); 144 | }); 145 | } 146 | 147 | protected function registerOrm(): void 148 | { 149 | $this->app->singleton(TransactionInterface::class, function () { 150 | return new Transaction( 151 | $this->app[ORMInterface::class] 152 | ); 153 | }); 154 | 155 | $this->app->singleton(SpiralContainerInterface::class, function () { 156 | $container = new \Spiral\Core\Container(); 157 | $container->bindSingleton(TransactionInterface::class, fn() => $this->app[TransactionInterface::class]); 158 | 159 | return $container; 160 | }); 161 | 162 | $this->app->singleton(FactoryInterface::class, function () { 163 | return new Factory( 164 | $this->app[DatabaseProviderInterface::class], 165 | null, 166 | $this->app[SpiralContainerInterface::class] 167 | ); 168 | }); 169 | 170 | $this->app->singleton(ORMInterface::class, function () { 171 | return new \Cycle\ORM\ORM( 172 | $this->app[FactoryInterface::class], 173 | $this->app[SchemaInterface::class] 174 | ); 175 | }); 176 | } 177 | 178 | protected function registerTokenizer(): void 179 | { 180 | $this->app->singleton(Tokenizer::class, function () { 181 | return new Tokenizer( 182 | new TokenizerConfig(config('cycle')) 183 | ); 184 | }); 185 | } 186 | 187 | protected function registerClassLocator(): void 188 | { 189 | $this->app->singleton(ClassLocator::class, function () { 190 | return $this->app[Tokenizer::class] 191 | ->classLocator(config('cycle.directories')); 192 | }); 193 | } 194 | 195 | protected function registerMigrationConfig(): void 196 | { 197 | $this->app->singleton(MigrationConfig::class, function () { 198 | return new MigrationConfig( 199 | (array)config('cycle.migrations') 200 | ); 201 | }); 202 | } 203 | 204 | protected function registerMigrator(): void 205 | { 206 | $this->app->singleton(Migrator::class, function () { 207 | $config = $this->app[MigrationConfig::class]; 208 | return new Migrator( 209 | $config, 210 | $this->app[DatabaseProviderInterface::class], 211 | new FileRepository($config), 212 | ); 213 | }); 214 | } 215 | 216 | protected function registerDatabaseFactory(): void 217 | { 218 | $this->app->singleton(FakerGenerator::class, function ($app, $parameters) { 219 | $locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US'); 220 | 221 | if (!isset(static::$fakers[$locale])) { 222 | static::$fakers[$locale] = FakerFactory::create($locale); 223 | } 224 | 225 | static::$fakers[$locale]->unique(true); 226 | 227 | return static::$fakers[$locale]; 228 | }); 229 | 230 | $this->app->singleton(\Butschster\Cycle\Database\Factory::class, function ($app) { 231 | $factory = new\Butschster\Cycle\Database\Factory( 232 | $app[ORMInterface::class], 233 | $app->make(FakerGenerator::class) 234 | ); 235 | 236 | return $factory->load( 237 | $this->app->databasePath('factories') 238 | ); 239 | }); 240 | } 241 | 242 | protected function registerSchemaManager(): void 243 | { 244 | $this->app->singleton(SchemaManagerContract::class, function () { 245 | return new SchemaManager( 246 | $this->app, 247 | $this->app[ClassLocator::class], 248 | $this->app[DatabaseProviderInterface::class], 249 | $this->app[\Illuminate\Contracts\Cache\Factory::class], 250 | $this->app[Migrator::class], 251 | $this->app[MigrationConfig::class] 252 | ); 253 | }); 254 | } 255 | } 256 | 257 | -------------------------------------------------------------------------------- /src/Repository.php: -------------------------------------------------------------------------------- 1 | transaction = $transaction; 24 | 25 | parent::__construct($select); 26 | } 27 | 28 | /** 29 | * Find multiple entities using given scope and sort options. 30 | * 31 | * @param array $scope 32 | * @param array $orderBy 33 | * 34 | * @return Collection 35 | */ 36 | public function findAll(array $scope = [], array $orderBy = []): Collection 37 | { 38 | return $this->newCollection( 39 | parent::findAll($scope, $orderBy) 40 | ); 41 | } 42 | 43 | /** 44 | * Paginate multiple entities using given scope and sort options 45 | * 46 | * @param array $scope 47 | * @param array $orderBy 48 | * @param int $perPage 49 | * @param int $page 50 | * @param string $pageName 51 | * @return Paginator 52 | */ 53 | public function paginate(array $scope = [], array $orderBy = [], int $perPage = 20, int $page = 1, $pageName = 'page'): Paginator 54 | { 55 | return $this->paginateQuery( 56 | $this->select()->where($scope)->orderBy($orderBy), 57 | $page, 58 | $perPage, 59 | $pageName, 60 | ); 61 | } 62 | 63 | /** 64 | * Persist the entity. 65 | * 66 | * @param object $entity 67 | * @param bool $cascade 68 | * @param bool $run Commit transaction 69 | * 70 | * @throws Throwable 71 | */ 72 | public function persist($entity, bool $cascade = true, bool $run = true): void 73 | { 74 | $this->transaction->persist( 75 | $entity, 76 | $cascade ? Transaction::MODE_CASCADE : Transaction::MODE_ENTITY_ONLY 77 | ); 78 | 79 | if ($run) { 80 | $this->transaction->run(); // transaction is clean after run 81 | } 82 | } 83 | 84 | /** 85 | * Delete entity from the database. 86 | * 87 | * @param $entity 88 | * @param bool $cascade 89 | * @param bool $run 90 | * @throws Throwable 91 | */ 92 | public function delete($entity, bool $cascade = true, bool $run = true) 93 | { 94 | $this->transaction->delete( 95 | $entity, 96 | $cascade ? Transaction::MODE_CASCADE : Transaction::MODE_ENTITY_ONLY 97 | ); 98 | 99 | if ($run) { 100 | $this->transaction->run(); // transaction is clean after run 101 | } 102 | } 103 | 104 | protected function paginateQuery(Select $query, $perPage = 20, int $page = 1, $pageName = 'page'): Paginator 105 | { 106 | return new Paginator( 107 | (new SpiralPaginator($perPage))->withPage($page)->paginate($query), 108 | $this->newCollection($query->fetchAll()), 109 | $pageName, 110 | ); 111 | } 112 | 113 | /** 114 | * Create a new collection of entities 115 | * 116 | * @param iterable $items 117 | * @return Collection 118 | */ 119 | protected function newCollection(iterable $items): Collection 120 | { 121 | return new Collection($items); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/SchemaManager.php: -------------------------------------------------------------------------------- 1 | classLocator = $classLocator; 43 | $this->databaseManager = $databaseManager; 44 | $this->cache = $cache; 45 | $this->migrator = $migrator; 46 | $this->config = $config; 47 | $this->app = $app; 48 | } 49 | 50 | public function isSyncMode(): bool 51 | { 52 | return (bool)config('cycle.schema.sync'); 53 | } 54 | 55 | public function createSchema(): ORMSchema 56 | { 57 | return new ORMSchema( 58 | $this->getCachedSchema() 59 | ); 60 | } 61 | 62 | public function flushSchemaData(): void 63 | { 64 | $this->getCacheStorage()->forget(self::SCHEMA_STORAGE_KEY); 65 | } 66 | 67 | protected function generateSchemaData(): array 68 | { 69 | return (new Schema\Compiler())->compile( 70 | new Schema\Registry($this->databaseManager), 71 | $this->getSchemaGenerators(), 72 | (array)config('cycle.schema.defaults') 73 | ); 74 | } 75 | 76 | protected function getSchemaGenerators(): array 77 | { 78 | $generators = [ 79 | new Schema\Generator\ResetTables(), 80 | new Annotated\Embeddings($this->classLocator), 81 | new Annotated\Entities($this->classLocator), 82 | new Annotated\MergeColumns(), 83 | new Schema\Generator\GenerateRelations(), 84 | new Schema\Generator\ValidateEntities(), 85 | new Schema\Generator\RenderTables(), 86 | new Schema\Generator\RenderRelations(), 87 | new Annotated\MergeIndexes(), 88 | new Schema\Generator\GenerateTypecast(), 89 | ]; 90 | 91 | // Если запускаем тесты, то синхронизируем схему на лету 92 | if ($this->isSyncMode()) { 93 | $generators[] = new Schema\Generator\SyncTables(); 94 | } else { // Для остальных случаев создаем миграции 95 | $generators[] = new GenerateMigrations( 96 | $this->migrator->getRepository(), 97 | $this->config 98 | ); 99 | } 100 | 101 | return $generators; 102 | } 103 | 104 | /** 105 | * @return CacheRepository 106 | */ 107 | protected function getCacheStorage(): CacheRepository 108 | { 109 | return $this->cache->store( 110 | config('cycle.schema.cache.storage') 111 | ); 112 | } 113 | 114 | protected function getCachedSchema(): array 115 | { 116 | if (!config('cycle.schema.cache.enabled')) { 117 | return $this->generateSchemaData(); 118 | } 119 | 120 | return $this->getCacheStorage()->rememberForever( 121 | self::SCHEMA_STORAGE_KEY, 122 | fn() => $this->generateSchemaData() 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/PaginatorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(11, $this->getPaginator()->total()); 13 | } 14 | 15 | function test_gets_last_page() 16 | { 17 | $this->assertEquals(3, $this->getPaginator()->lastPage()); 18 | } 19 | 20 | function test_gets_next_page_url_when_it_exists() 21 | { 22 | $this->assertEquals('/?test_page=3', $this->getPaginator()->nextPageUrl()); 23 | } 24 | 25 | function test_gets_next_page_url_when_it_not_exists() 26 | { 27 | $this->assertNull( 28 | $this->getPaginator(1, 1, 1)->nextPageUrl() 29 | ); 30 | } 31 | 32 | function test_gets_previous_page_url_when_it_exists() 33 | { 34 | $this->assertEquals('/?test_page=1', $this->getPaginator()->previousPageUrl()); 35 | } 36 | 37 | function test_gets_previous_page_url_when_it_not_exists() 38 | { 39 | $this->assertNull( 40 | $this->getPaginator(1, 1, 1)->previousPageUrl() 41 | ); 42 | } 43 | 44 | function test_gets_items() 45 | { 46 | $this->assertEquals([1, 2, 3, 4, 5], $this->getPaginator()->items()); 47 | } 48 | 49 | function test_gets_first_item() 50 | { 51 | $this->assertEquals(6, $this->getPaginator()->firstItem()); 52 | } 53 | 54 | function test_gets_last_item() 55 | { 56 | $this->assertEquals(10, $this->getPaginator()->lastItem()); 57 | } 58 | 59 | function test_gets_per_page() 60 | { 61 | $this->assertEquals(5, $this->getPaginator()->perPage()); 62 | } 63 | 64 | function test_gets_current_page() 65 | { 66 | $this->assertEquals(2, $this->getPaginator()->currentPage()); 67 | } 68 | 69 | function test_gets_page_name() 70 | { 71 | $this->assertEquals('test_page', $this->getPaginator()->getPageName()); 72 | } 73 | 74 | function test_gets_count() 75 | { 76 | $this->assertEquals(5, $this->getPaginator()->count()); 77 | } 78 | 79 | function test_gets_collection() 80 | { 81 | $this->assertInstanceOf(Collection::class, $this->getPaginator()->getCollection()); 82 | } 83 | 84 | /** 85 | * @param int $totalPages 86 | * @param int $perPage 87 | * @param int $currentPage 88 | * @return Paginator 89 | */ 90 | protected function getPaginator(int $totalPages = 11, int $perPage = 5, int $currentPage = 2): Paginator 91 | { 92 | return new Paginator( 93 | 94 | (new \Spiral\Pagination\Paginator($perPage)) 95 | ->withPage($currentPage) 96 | ->withCount($totalPages), 97 | 98 | new Collection(range(1, $perPage)), 99 | 'test_page' 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |