├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer-require-checker.json ├── composer.json ├── infection.json.dist ├── psalm.xml ├── rector.php ├── src ├── .meta-storm.xml ├── AbstractActiveRecord.php ├── ActiveQuery.php ├── ActiveQueryInterface.php ├── ActiveQueryTrait.php ├── ActiveRecord.php ├── ActiveRecordInterface.php ├── ActiveRelationTrait.php ├── ArArrayHelper.php ├── ConnectionProvider.php ├── ConnectionProviderMiddleware.php ├── NotFoundException.php ├── OptimisticLockException.php ├── OptimisticLockInterface.php └── Trait │ ├── ArrayAccessTrait.php │ ├── ArrayIteratorTrait.php │ ├── ArrayableTrait.php │ ├── CustomConnectionTrait.php │ ├── CustomTableNameTrait.php │ ├── FactoryTrait.php │ ├── MagicPropertiesTrait.php │ ├── MagicRelationsTrait.php │ └── RepositoryTrait.php └── tests ├── ActiveQueryFindTest.php ├── ActiveQueryTest.php ├── ActiveRecordTest.php ├── ArrayableTraitTest.php ├── BatchQueryResultTest.php ├── ConnectionProviderTest.php ├── Driver ├── Mssql │ ├── ActiveQueryFindTest.php │ ├── ActiveQueryTest.php │ ├── ActiveRecordTest.php │ ├── ArrayableTraitTest.php │ ├── BatchQueryResultTest.php │ ├── ConnectionProviderTest.php │ ├── MagicActiveRecordTest.php │ └── RepositoryTraitTest.php ├── Mysql │ ├── ActiveQueryFindTest.php │ ├── ActiveQueryTest.php │ ├── ActiveRecordTest.php │ ├── ArrayableTraitTest.php │ ├── BatchQueryResultTest.php │ ├── ConnectionProviderTest.php │ ├── MagicActiveRecordTest.php │ ├── RepositoryTraitTest.php │ └── Stubs │ │ └── Type.php ├── Oracle │ ├── ActiveQueryFindTest.php │ ├── ActiveQueryTest.php │ ├── ActiveRecordTest.php │ ├── ArrayableTraitTest.php │ ├── BatchQueryResultTest.php │ ├── ConnectionProviderTest.php │ ├── MagicActiveRecordTest.php │ ├── RepositoryTraitTest.php │ └── Stubs │ │ ├── Customer.php │ │ ├── MagicCustomer.php │ │ ├── MagicOrder.php │ │ └── Order.php ├── Pgsql │ ├── ActiveQueryFindTest.php │ ├── ActiveQueryTest.php │ ├── ActiveRecordTest.php │ ├── ArrayableTraitTest.php │ ├── BatchQueryResultTest.php │ ├── ConnectionProviderTest.php │ ├── MagicActiveRecordTest.php │ ├── RepositoryTraitTest.php │ └── Stubs │ │ ├── Item.php │ │ ├── Promotion.php │ │ └── Type.php └── Sqlite │ ├── ActiveQueryFindTest.php │ ├── ActiveQueryTest.php │ ├── ActiveRecordTest.php │ ├── ArrayableTraitTest.php │ ├── BatchQueryResultTest.php │ ├── ConnectionProviderTest.php │ ├── MagicActiveRecordTest.php │ └── RepositoryTraitTest.php ├── MagicActiveRecordTest.php ├── RepositoryTraitTest.php ├── Stubs ├── ActiveRecord │ ├── Alpha.php │ ├── Animal.php │ ├── ArrayAndJsonTypes.php │ ├── Beta.php │ ├── BitValues.php │ ├── BoolAR.php │ ├── Cat.php │ ├── Category.php │ ├── Customer.php │ ├── CustomerClosureField.php │ ├── CustomerForArrayable.php │ ├── CustomerQuery.php │ ├── CustomerWithAlias.php │ ├── CustomerWithCustomConnection.php │ ├── CustomerWithFactory.php │ ├── DefaultPk.php │ ├── Department.php │ ├── Document.php │ ├── Dog.php │ ├── Dossier.php │ ├── Employee.php │ ├── Item.php │ ├── NoExist.php │ ├── NullValues.php │ ├── Order.php │ ├── OrderItem.php │ ├── OrderItemWithNullFK.php │ ├── OrderWithFactory.php │ ├── OrderWithNullFK.php │ ├── Profile.php │ ├── ProfileWithConstructor.php │ ├── Promotion.php │ ├── TestTrigger.php │ ├── TestTriggerAlert.php │ ├── Type.php │ ├── UnqueryableQueryMock.php │ └── UserAR.php ├── ArrayableActiveRecord.php ├── MagicActiveRecord.php └── MagicActiveRecord │ ├── Alpha.php │ ├── Animal.php │ ├── ArrayAndJsonTypes.php │ ├── Beta.php │ ├── BitValues.php │ ├── BoolAR.php │ ├── Cat.php │ ├── Category.php │ ├── Customer.php │ ├── CustomerQuery.php │ ├── CustomerWithAlias.php │ ├── CustomerWithProperties.php │ ├── DefaultPk.php │ ├── Department.php │ ├── Document.php │ ├── Dog.php │ ├── Dossier.php │ ├── Employee.php │ ├── Item.php │ ├── NoExist.php │ ├── NullValues.php │ ├── Order.php │ ├── OrderItem.php │ ├── OrderItemWithNullFK.php │ ├── OrderWithNullFK.php │ ├── Profile.php │ ├── ProfileWithConstructor.php │ ├── TestTrigger.php │ ├── TestTriggerAlert.php │ ├── Type.php │ ├── UnqueryableQueryMock.php │ └── UserAR.php ├── Support ├── Assert.php ├── ConnectionHelper.php ├── DbHelper.php ├── ModelFactory.php ├── MssqlHelper.php ├── MysqlHelper.php ├── OracleHelper.php ├── PgsqlHelper.php └── SqliteHelper.php ├── TestCase.php └── data ├── mssql.sql ├── mysql.sql ├── oci.sql ├── pgsql.sql ├── runtime └── .gitignore └── sqlite.sql /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii Active Record Change Log 2 | 3 | ## 1.0.0 under development 4 | 5 | - Enh #40: Added public method `yii\data\ActiveDataProvider::prepareQuery()` for debug facilitation (GHopperMSK) 6 | - Enh #34: Adjustments to new DI and events replacements (fabriziocaldarelli) 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "Psr\\Http\\Message\\ResponseInterface", 4 | "Psr\\Http\\Message\\ServerRequestInterface", 5 | "Psr\\Http\\Server\\MiddlewareInterface", 6 | "Psr\\Http\\Server\\RequestHandlerInterface", 7 | "Yiisoft\\Arrays\\ArrayableTrait", 8 | "Yiisoft\\Factory\\Factory" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/active-record", 3 | "type": "library", 4 | "description": "Yii ActiveRecord Library", 5 | "keywords": [ 6 | "yii", 7 | "active record" 8 | ], 9 | "homepage": "https://www.yiiframework.com/", 10 | "license": "BSD-3-Clause", 11 | "support": { 12 | "issues": "https://github.com/yiisoft/active-record/issues?state=open", 13 | "source": "https://github.com/yiisoft/active-record", 14 | "forum": "https://www.yiiframework.com/forum/", 15 | "wiki": "https://www.yiiframework.com/wiki/", 16 | "irc": "ircs://irc.libera.chat:6697/yii", 17 | "chat": "https://t.me/yii3en" 18 | }, 19 | "funding": [ 20 | { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/yiisoft" 23 | }, 24 | { 25 | "type": "github", 26 | "url": "https://github.com/sponsors/yiisoft" 27 | } 28 | ], 29 | "minimum-stability": "dev", 30 | "prefer-stable": true, 31 | "require": { 32 | "php": "8.1 - 8.4", 33 | "yiisoft/db": "dev-master" 34 | }, 35 | "require-dev": { 36 | "maglnet/composer-require-checker": "^4.7.1", 37 | "phpunit/phpunit": "^10.5.45", 38 | "rector/rector": "^2.0.10", 39 | "roave/infection-static-analysis-plugin": "^1.35", 40 | "spatie/phpunit-watcher": "^1.24", 41 | "vimeo/psalm": "^5.26.1 || ^6.8.8", 42 | "yiisoft/aliases": "^2.0", 43 | "yiisoft/arrays": "^3.2", 44 | "yiisoft/cache": "^3.0", 45 | "yiisoft/db-sqlite": "dev-master", 46 | "yiisoft/di": "^1.3", 47 | "yiisoft/factory": "^1.3", 48 | "yiisoft/middleware-dispatcher": "^5.2" 49 | }, 50 | "suggest": { 51 | "yiisoft/arrays": "For \\Yiisoft\\Arrays\\ArrayableInterface support", 52 | "yiisoft/db-sqlite": "For SQLite database support", 53 | "yiisoft/db-mysql": "For MySQL database support", 54 | "yiisoft/db-pgsql": "For PostgreSQL database support", 55 | "yiisoft/db-mssql": "For MSSQL database support", 56 | "yiisoft/db-oracle": "For Oracle database support", 57 | "yiisoft/factory": "For factory support", 58 | "yiisoft/middleware-dispatcher": "For middleware support" 59 | }, 60 | "autoload": { 61 | "psr-4": { 62 | "Yiisoft\\ActiveRecord\\": "src" 63 | } 64 | }, 65 | "autoload-dev": { 66 | "psr-4": { 67 | "Yiisoft\\ActiveRecord\\Tests\\": "tests" 68 | } 69 | }, 70 | "extra": { 71 | "branch-alias": { 72 | "dev-master": "3.0.x-dev" 73 | } 74 | }, 75 | "config": { 76 | "sort-packages": true, 77 | "allow-plugins": { 78 | "infection/extension-installer": true, 79 | "composer/package-versions-deprecated": true 80 | } 81 | }, 82 | "scripts": { 83 | "test": "phpunit --testdox --no-interaction", 84 | "test-watch": "phpunit-watcher watch" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "timeout": 45, 8 | "logs": { 9 | "text": "php:\/\/stderr", 10 | "stryker": { 11 | "report": "master" 12 | } 13 | }, 14 | "mutators": { 15 | "@default": true, 16 | "ArrayItemRemoval": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 13 | __DIR__ . '/src', 14 | /** Disable rector on tests */ 15 | // __DIR__ . '/tests', 16 | ]); 17 | 18 | // register a single rule 19 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 20 | 21 | // define sets of rules 22 | $rectorConfig->sets([ 23 | LevelSetList::UP_TO_PHP_81, 24 | ]); 25 | 26 | $rectorConfig->skip([ 27 | NullToStrictStringFuncCallArgRector::class, 28 | ReadOnlyPropertyRector::class, 29 | ]); 30 | }; 31 | -------------------------------------------------------------------------------- /src/.meta-storm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ActiveQueryTrait.php: -------------------------------------------------------------------------------- 1 | asArray = $value; 34 | return $this; 35 | } 36 | 37 | /** 38 | * Specifies the relations with which this query should be performed. 39 | * 40 | * The parameters to this method can be either one or multiple strings, or a single array of relation names and the 41 | * optional callbacks to customize the relations. 42 | * 43 | * A relation name can refer to a relation defined in {@see modelClass} or a sub-relation that stands for a relation 44 | * of a related record. 45 | * 46 | * For example, `orders.address` means the `address` relation defined in the model class corresponding to the 47 | * `orders` relation. 48 | * 49 | * The following are some usage examples: 50 | * 51 | * ```php 52 | * // Create active query 53 | * CustomerQuery = new ActiveQuery(Customer::class); 54 | * // find customers together with their orders and country 55 | * CustomerQuery->with('orders', 'country')->all(); 56 | * // find customers together with their orders and the orders' shipping address 57 | * CustomerQuery->with('orders.address')->all(); 58 | * // find customers together with their country and orders of status 1 59 | * CustomerQuery->with([ 60 | * 'orders' => function (ActiveQuery $query) { 61 | * $query->andWhere('status = 1'); 62 | * }, 63 | * 'country', 64 | * ])->all(); 65 | * ``` 66 | * 67 | * You can call `with()` multiple times. Each call will add relations to the existing ones. 68 | * 69 | * For example, the following two statements are equivalent: 70 | * 71 | * ```php 72 | * CustomerQuery->with('orders', 'country')->all(); 73 | * CustomerQuery->with('orders')->with('country')->all(); 74 | * ``` 75 | * 76 | * @param array|string ...$with A list of relation names or relation definitions. 77 | * 78 | * @return static The query object itself. 79 | */ 80 | public function with(array|string ...$with): static 81 | { 82 | if (isset($with[0]) && is_array($with[0])) { 83 | /** the parameter is given as an array */ 84 | $with = $with[0]; 85 | } 86 | 87 | if (empty($this->with)) { 88 | $this->with = $with; 89 | } elseif (!empty($with)) { 90 | foreach ($with as $name => $value) { 91 | if (is_int($name)) { 92 | /** repeating relation is fine as normalizeRelations() handle it well */ 93 | $this->with[] = $value; 94 | } else { 95 | $this->with[$name] = $value; 96 | } 97 | } 98 | } 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Converts found rows into model instances. 105 | * 106 | * @param array[] $rows The rows to be converted. 107 | * 108 | * @throws InvalidConfigException 109 | * @return ActiveRecordInterface[]|array[] The model instances. 110 | * 111 | * @psalm-param non-empty-list $rows 112 | * @psalm-return non-empty-list 113 | */ 114 | protected function createModels(array $rows): array 115 | { 116 | if ($this->asArray) { 117 | return $rows; 118 | } 119 | 120 | if ($this->resultCallback !== null) { 121 | $rows = ($this->resultCallback)($rows); 122 | 123 | if ($rows[0] instanceof ActiveRecordInterface) { 124 | /** @psalm-var non-empty-list */ 125 | return $rows; 126 | } 127 | } 128 | 129 | $models = []; 130 | 131 | foreach ($rows as $row) { 132 | $arClass = $this->getARInstance(); 133 | $arClass->populateRecord($row); 134 | 135 | $models[] = $arClass; 136 | } 137 | 138 | return $models; 139 | } 140 | 141 | /** 142 | * Finds records corresponding to one or multiple relations and populates them into the primary models. 143 | * 144 | * @param array $with a list of relations that this query should be performed with. Please refer to {@see with()} 145 | * for details about specifying this parameter. 146 | * @param ActiveRecordInterface[]|array[] $models the primary models (can be either AR instances or arrays) 147 | * 148 | * @throws Exception 149 | * @throws InvalidArgumentException 150 | * @throws NotSupportedException 151 | * @throws ReflectionException 152 | * @throws Throwable 153 | * 154 | * @psalm-param non-empty-list $models 155 | * @psalm-param-out non-empty-list $models 156 | */ 157 | public function findWith(array $with, array &$models): void 158 | { 159 | $primaryModel = reset($models); 160 | 161 | if (!$primaryModel instanceof ActiveRecordInterface) { 162 | $primaryModel = $this->getARInstance(); 163 | } 164 | 165 | $relations = $this->normalizeRelations($primaryModel, $with); 166 | 167 | foreach ($relations as $name => $relation) { 168 | if ($relation->isAsArray() === null) { 169 | /** inherit asArray from a primary query */ 170 | $relation->asArray($this->asArray); 171 | } 172 | 173 | $relation->populateRelation($name, $models); 174 | } 175 | } 176 | 177 | /** 178 | * @return ActiveQueryInterface[] 179 | */ 180 | private function normalizeRelations(ActiveRecordInterface $model, array $with): array 181 | { 182 | $relations = []; 183 | 184 | foreach ($with as $name => $callback) { 185 | if (is_int($name)) { 186 | $name = $callback; 187 | $callback = null; 188 | } 189 | 190 | if (($pos = strpos($name, '.')) !== false) { 191 | /** with sub-relations */ 192 | $childName = substr($name, $pos + 1); 193 | $name = substr($name, 0, $pos); 194 | } else { 195 | $childName = null; 196 | } 197 | 198 | if (!isset($relations[$name])) { 199 | /** @var ActiveQuery $relation */ 200 | $relation = $model->relationQuery($name); 201 | $relation->primaryModel = null; 202 | $relations[$name] = $relation; 203 | } else { 204 | $relation = $relations[$name]; 205 | } 206 | 207 | if (isset($childName)) { 208 | $relation->with[$childName] = $callback; 209 | } elseif ($callback !== null) { 210 | $callback($relation); 211 | } 212 | } 213 | 214 | return $relations; 215 | } 216 | 217 | public function isAsArray(): bool|null 218 | { 219 | return $this->asArray; 220 | } 221 | 222 | public function getWith(): array 223 | { 224 | return $this->with; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/ActiveRecord.php: -------------------------------------------------------------------------------- 1 | name`. 30 | * 31 | * In this example, Active Record is providing an object-oriented interface for accessing data stored in the database. 32 | * But Active Record provides much more functionality than this. 33 | * 34 | * To declare an ActiveRecord class, you need to extend {@see ActiveRecord} and implement the `tableName` method: 35 | * 36 | * ```php 37 | * class Customer extends ActiveRecord 38 | * { 39 | * public static function tableName(): string 40 | * { 41 | * return 'customer'; 42 | * } 43 | * } 44 | * ``` 45 | * 46 | * The `tableName` method only has to return the name of the database table associated with the class. 47 | * 48 | * Class instances are obtained in one of two ways: 49 | * 50 | * Using the `new` operator to create a new, empty object. 51 | * Using a method to fetch an existing record (or records) from the database. 52 | * 53 | * Below is an example showing some typical usage of ActiveRecord: 54 | * 55 | * ```php 56 | * $user = new User(); 57 | * $user->name = 'Qiang'; 58 | * $user->save(); // a new row is inserted into user table 59 | * 60 | * // the following will retrieve the user 'CeBe' from the database 61 | * $userQuery = new ActiveQuery(User::class); 62 | * $user = $userQuery->where(['name' => 'CeBe'])->one(); 63 | * 64 | * // this will get related records from orders table when relation is defined 65 | * $orders = $user->orders; 66 | * ``` 67 | * 68 | * For more details and usage information on ActiveRecord, 69 | * {@see the [guide article on ActiveRecord](guide:db-active-record)} 70 | * 71 | * @psalm-suppress ClassMustBeFinal 72 | */ 73 | class ActiveRecord extends AbstractActiveRecord 74 | { 75 | public function propertyNames(): array 76 | { 77 | return $this->tableSchema()->getColumnNames(); 78 | } 79 | 80 | public function columnType(string $propertyName): string 81 | { 82 | return $this->tableSchema()->getColumn($propertyName)?->getType() ?? ColumnType::STRING; 83 | } 84 | 85 | /** 86 | * Returns the schema information of the DB table associated with this AR class. 87 | * 88 | * @throws Exception 89 | * @throws InvalidConfigException If the table for the AR class doesn't exist. 90 | * 91 | * @return TableSchemaInterface The schema information of the DB table associated with this AR class. 92 | */ 93 | public function tableSchema(): TableSchemaInterface 94 | { 95 | $tableSchema = $this->db()->getSchema()->getTableSchema($this->tableName()); 96 | 97 | if ($tableSchema === null) { 98 | throw new InvalidConfigException('The table does not exist: ' . $this->tableName()); 99 | } 100 | 101 | return $tableSchema; 102 | } 103 | 104 | /** 105 | * Loads default values from database table schema. 106 | * 107 | * You may call this method to load default values after creating a new instance: 108 | * 109 | * ```php 110 | * // class Customer extends ActiveRecord 111 | * $customer = new Customer(); 112 | * $customer->loadDefaultValues(); 113 | * ``` 114 | * 115 | * @param bool $skipIfSet Whether existing value should be preserved. This will only set defaults for properties 116 | * that are `null`. 117 | * 118 | * @throws Exception 119 | * @throws InvalidConfigException 120 | * 121 | * @return static The active record instance itself. 122 | */ 123 | public function loadDefaultValues(bool $skipIfSet = true): static 124 | { 125 | foreach ($this->tableSchema()->getColumns() as $name => $column) { 126 | if ($column->getDefaultValue() !== null && (!$skipIfSet || $this->get($name) === null)) { 127 | $this->set($name, $column->getDefaultValue()); 128 | } 129 | } 130 | 131 | return $this; 132 | } 133 | 134 | public function populateRecord(array|object $row): void 135 | { 136 | $row = ArArrayHelper::toArray($row); 137 | $columns = $this->tableSchema()->getColumns(); 138 | $rowColumns = array_intersect_key($row, $columns); 139 | 140 | foreach ($rowColumns as $name => &$value) { 141 | $value = $columns[$name]->phpTypecast($value); 142 | } 143 | 144 | parent::populateRecord($rowColumns + $row); 145 | } 146 | 147 | public function primaryKey(): array 148 | { 149 | return $this->tableSchema()->getPrimaryKey(); 150 | } 151 | 152 | protected function propertyValuesInternal(): array 153 | { 154 | return get_object_vars($this); 155 | } 156 | 157 | protected function insertInternal(array|null $properties = null): bool 158 | { 159 | if (!$this->isNewRecord()) { 160 | throw new InvalidCallException('The record is not new and cannot be inserted.'); 161 | } 162 | 163 | $values = $this->newPropertyValues($properties); 164 | $primaryKeys = $this->db()->createCommand()->insertWithReturningPks($this->tableName(), $values); 165 | 166 | if ($primaryKeys === false) { 167 | return false; 168 | } 169 | 170 | $columns = $this->tableSchema()->getColumns(); 171 | 172 | foreach ($primaryKeys as $name => $value) { 173 | $id = $columns[$name]->phpTypecast($value); 174 | $this->set($name, $id); 175 | $values[$name] = $id; 176 | } 177 | 178 | $this->assignOldValues($values); 179 | 180 | return true; 181 | } 182 | 183 | protected function populateProperty(string $name, mixed $value): void 184 | { 185 | $this->$name = $value; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/ArArrayHelper.php: -------------------------------------------------------------------------------- 1 | '123', 'data' => 'abc'], 38 | * ['id' => '345', 'data' => 'def'], 39 | * ]; 40 | * $result = ArArrayHelper::getColumn($array, 'id'); 41 | * // the result is: ['123', '345'] 42 | * ``` 43 | * 44 | * @param ActiveRecordInterface[]|array[] $array Array to extract values from. 45 | * @param string $name The column name. 46 | * 47 | * @return array The list of column values. 48 | */ 49 | public static function getColumn(array $array, string $name): array 50 | { 51 | return array_map( 52 | static fn (ActiveRecordInterface|array $element): mixed => self::getValueByPath($element, $name), 53 | $array 54 | ); 55 | } 56 | 57 | /** 58 | * Retrieves a value from the array by the given key or from the {@see ActiveRecordInterface} instance 59 | * by the given property or relation name. 60 | * 61 | * If the key doesn't exist, the default value will be returned instead. 62 | * 63 | * The key may be specified in a dot format to retrieve the value of a sub-array or a property or relation of the 64 | * {@see ActiveRecordInterface} instance. 65 | * 66 | * In particular, if the key is `x.y.z`, then the returned value would be `$array['x']['y']['z']` or 67 | * `$array->x->y->z` (if `$array` is an {@see ActiveRecordInterface} instance). 68 | * 69 | * Note that if the array already has an element `x.y.z`, then its value will be returned instead of going through 70 | * the sub-arrays. 71 | * 72 | * Below are some usage examples. 73 | * 74 | * ```php 75 | * // working with an array 76 | * $username = ArArrayHelper::getValueByPath($array, 'username'); 77 | * // working with an {@see ActiveRecordInterface} instance 78 | * $username = ArArrayHelper::getValueByPath($user, 'username'); 79 | * // using dot format to retrieve the property of an {@see ActiveRecordInterface} instance 80 | * $street = ArArrayHelper::getValue($users, 'address.street'); 81 | * ``` 82 | * 83 | * @param ActiveRecordInterface|array $array Array or an {@see ActiveRecordInterface} instance to extract value from. 84 | * @param string $key Key name of the array element or a property or relation name 85 | * of the {@see ActiveRecordInterface} instance. 86 | * @param mixed|null $default The default value to be returned if the specified `$key` doesn't exist. 87 | * 88 | * @return mixed The value of the element if found, default value otherwise 89 | */ 90 | public static function getValueByPath(ActiveRecordInterface|array $array, string $key, mixed $default = null): mixed 91 | { 92 | if ($array instanceof ActiveRecordInterface) { 93 | if ($array->hasProperty($key)) { 94 | return $array->get($key); 95 | } 96 | 97 | if (property_exists($array, $key)) { 98 | return array_key_exists($key, get_object_vars($array)) ? $array->$key : $default; 99 | } 100 | 101 | if ($array->isRelationPopulated($key)) { 102 | return $array->relation($key); 103 | } 104 | } elseif (array_key_exists($key, $array)) { 105 | return $array[$key]; 106 | } 107 | 108 | if (!empty($key) && ($pos = strrpos($key, '.')) !== false) { 109 | $array = self::getValueByPath($array, substr($key, 0, $pos), $default); 110 | $key = substr($key, $pos + 1); 111 | 112 | return self::getValueByPath($array, $key, $default); 113 | } 114 | 115 | return $default; 116 | } 117 | 118 | /** 119 | * Indexes an array of rows with the specified column value as keys. 120 | * 121 | * The input array should be multidimensional or an array of {@see ActiveRecordInterface} instances. 122 | * 123 | * For example, 124 | * 125 | * ```php 126 | * $rows = [ 127 | * ['id' => '123', 'data' => 'abc'], 128 | * ['id' => '345', 'data' => 'def'], 129 | * ]; 130 | * $result = ArArrayHelper::populate($rows, 'id'); 131 | * // the result is: ['123' => ['id' => '123', 'data' => 'abc'], '345' => ['id' => '345', 'data' => 'def']] 132 | * ``` 133 | * 134 | * @param ActiveRecordInterface[]|array[] $rows Array to populate. 135 | * @param Closure|string|null $indexBy The column name or anonymous function that specifies the index by which to 136 | * populate the array of rows. 137 | * 138 | * @return ActiveRecordInterface[]|array[] 139 | * 140 | * @psalm-template TRow of Row 141 | * @psalm-param array $rows 142 | * @psalm-param IndexKey|null $indexBy 143 | * @psalm-return array 144 | */ 145 | public static function index(array $rows, Closure|string|null $indexBy = null): array 146 | { 147 | if ($indexBy === null) { 148 | return $rows; 149 | } 150 | 151 | if ($indexBy instanceof Closure) { 152 | return array_combine(array_map($indexBy, $rows), $rows); 153 | } 154 | 155 | $result = []; 156 | 157 | foreach ($rows as $row) { 158 | /** @psalm-suppress MixedArrayOffset */ 159 | $result[self::getValueByPath($row, $indexBy)] = $row; 160 | } 161 | 162 | return $result; 163 | } 164 | 165 | /** 166 | * Converts an object into an array. 167 | * 168 | * @param array|object $object The object to be converted into an array. 169 | * 170 | * @return array The array representation of the object. 171 | */ 172 | public static function toArray(array|object $object): array 173 | { 174 | if (is_array($object)) { 175 | return $object; 176 | } 177 | 178 | if ($object instanceof ActiveRecordInterface) { 179 | return $object->propertyValues(); 180 | } 181 | 182 | if ($object instanceof Traversable) { 183 | return iterator_to_array($object); 184 | } 185 | 186 | return get_object_vars($object); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/ConnectionProvider.php: -------------------------------------------------------------------------------- 1 | db); 22 | 23 | return $handler->handle($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/NotFoundException.php: -------------------------------------------------------------------------------- 1 | hasProperty($offset)) { 53 | return $this->get($offset) !== null; 54 | } 55 | 56 | if (property_exists($this, $offset)) { 57 | return isset($this->$offset); 58 | } 59 | 60 | if ($this->isRelationPopulated($offset)) { 61 | return $this->relation($offset) !== null; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | /** 68 | * @param string $offset The offset to retrieve element. 69 | */ 70 | public function offsetGet(mixed $offset): mixed 71 | { 72 | if ($this->hasProperty($offset)) { 73 | return $this->get($offset); 74 | } 75 | 76 | if (property_exists($this, $offset)) { 77 | return $this->$offset ?? null; 78 | } 79 | 80 | return $this->relation($offset); 81 | } 82 | 83 | /** 84 | * Sets the element at the specified offset. 85 | * 86 | * This method is required by the SPL interface {@see ArrayAccess}. 87 | * 88 | * It is implicitly called when you use something like `$model[$offset] = $item;`. 89 | * 90 | * @param string $offset The offset to set element. 91 | */ 92 | public function offsetSet(mixed $offset, mixed $value): void 93 | { 94 | if ($this->hasProperty($offset)) { 95 | $this->set($offset, $value); 96 | return; 97 | } 98 | 99 | if (property_exists($this, $offset)) { 100 | $this->$offset = $value; 101 | return; 102 | } 103 | 104 | if ($value instanceof ActiveRecordInterface || is_array($value) || $value === null) { 105 | $this->populateRelation($offset, $value); 106 | return; 107 | } 108 | 109 | throw new InvalidArgumentException('Setting unknown property: ' . static::class . '::' . $offset); 110 | } 111 | 112 | /** 113 | * Sets the element value at the specified offset to null. 114 | * 115 | * This method is required by the SPL interface {@see ArrayAccess}. 116 | * 117 | * It is implicitly called when you use something like `unset($model[$offset])`. 118 | * 119 | * @param string $offset The offset to unset element. 120 | */ 121 | public function offsetUnset(mixed $offset): void 122 | { 123 | if ($this->hasProperty($offset) || property_exists($this, $offset)) { 124 | $this->set($offset, null); 125 | unset($this->$offset); 126 | return; 127 | } 128 | 129 | $this->resetRelation($offset); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Trait/ArrayIteratorTrait.php: -------------------------------------------------------------------------------- 1 | propertyValues(); 28 | 29 | return new ArrayIterator($values); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Trait/ArrayableTrait.php: -------------------------------------------------------------------------------- 1 | relatedRecords()); 34 | 35 | return array_combine($fields, $fields); 36 | } 37 | 38 | /** 39 | * @psalm-return array 40 | */ 41 | public function fields(): array 42 | { 43 | $fields = $this->propertyNames(); 44 | 45 | return array_combine($fields, $fields); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Trait/CustomConnectionTrait.php: -------------------------------------------------------------------------------- 1 | connectionName = $connectionName; 27 | return $new; 28 | } 29 | 30 | public function db(): ConnectionInterface 31 | { 32 | if (!empty($this->connectionName)) { 33 | return ConnectionProvider::get($this->connectionName); 34 | } 35 | 36 | return parent::db(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Trait/CustomTableNameTrait.php: -------------------------------------------------------------------------------- 1 | tableName = $tableName; 23 | return $new; 24 | } 25 | 26 | public function tableName(): string 27 | { 28 | return $this->tableName ??= parent::tableName(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Trait/FactoryTrait.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 32 | return $new; 33 | } 34 | 35 | public function query(ActiveRecordInterface|Closure|null|string $arClass = null): ActiveQueryInterface 36 | { 37 | if ($arClass === null) { 38 | return new ActiveQuery($this); 39 | } 40 | 41 | if (!isset($this->factory)) { 42 | return new ActiveQuery($arClass); 43 | } 44 | 45 | if (is_string($arClass)) { 46 | if (method_exists($arClass, 'withFactory')) { 47 | return new ActiveQuery( 48 | fn (): ActiveRecordInterface => $this->factory->create($arClass)->withFactory($this->factory) 49 | ); 50 | } 51 | 52 | return new ActiveQuery(fn (): ActiveRecordInterface => $this->factory->create($arClass)); 53 | } 54 | 55 | if ($arClass instanceof ActiveRecordInterface && method_exists($arClass, 'withFactory')) { 56 | return new ActiveQuery($arClass->withFactory($this->factory)); 57 | } 58 | 59 | return new ActiveQuery($arClass); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Trait/MagicPropertiesTrait.php: -------------------------------------------------------------------------------- 1 | $propertyValues */ 46 | private array $propertyValues = []; 47 | 48 | /** 49 | * PHP getter magic method. 50 | * This method is overridden so that values and related objects can be accessed like properties. 51 | * 52 | * @param string $name Property or relation name. 53 | * 54 | * @throws InvalidArgumentException|InvalidCallException|InvalidConfigException|ReflectionException|Throwable 55 | * @throws UnknownPropertyException 56 | * 57 | * @throws Exception 58 | * @return mixed Property or relation value. 59 | * 60 | * @see get() 61 | */ 62 | public function __get(string $name) 63 | { 64 | if (method_exists($this, $getter = "get$name")) { 65 | /** Read getter, e.g., getName() */ 66 | return $this->$getter(); 67 | } 68 | 69 | if ($this->hasProperty($name)) { 70 | return $this->get($name); 71 | } 72 | 73 | if ($this->isRelationPopulated($name)) { 74 | return $this->relatedRecords()[$name]; 75 | } 76 | 77 | if (method_exists($this, "get{$name}Query")) { 78 | /** Read relation query getter, e.g., getUserQuery() */ 79 | return $this->retrieveRelation($name); 80 | } 81 | 82 | if (method_exists($this, "set$name")) { 83 | throw new InvalidCallException('Getting write-only property: ' . static::class . '::' . $name); 84 | } 85 | 86 | throw new UnknownPropertyException('Getting unknown property or relation: ' . static::class . '::' . $name); 87 | } 88 | 89 | /** 90 | * PHP isset magic method. 91 | * Checks if a property or relation exists and its value is not `null`. 92 | * 93 | * @param string $name The property or relation name. 94 | */ 95 | public function __isset(string $name): bool 96 | { 97 | try { 98 | return $this->__get($name) !== null; 99 | } catch (InvalidCallException|UnknownPropertyException) { 100 | return false; 101 | } 102 | } 103 | 104 | /** 105 | * PHP unset magic method. 106 | * Unsets the property or relation. 107 | * 108 | * @param string $name The property or relation name. 109 | */ 110 | public function __unset(string $name): void 111 | { 112 | if ($this->hasProperty($name)) { 113 | unset($this->propertyValues[$name]); 114 | 115 | if ($this->hasDependentRelations($name)) { 116 | $this->resetDependentRelations($name); 117 | } 118 | } elseif ($this->isRelationPopulated($name)) { 119 | $this->resetRelation($name); 120 | } 121 | } 122 | 123 | /** 124 | * PHP setter magic method. 125 | * Sets the value of a property. 126 | * 127 | * @param string $name Property name. 128 | * 129 | * @throws InvalidCallException|UnknownPropertyException 130 | */ 131 | public function __set(string $name, mixed $value): void 132 | { 133 | if (method_exists($this, $setter = "set$name")) { 134 | $this->$setter($value); 135 | return; 136 | } 137 | 138 | if ($this->hasProperty($name)) { 139 | parent::set($name, $value); 140 | return; 141 | } 142 | 143 | if ( 144 | method_exists($this, "get$name") 145 | || method_exists($this, "get{$name}Query") 146 | ) { 147 | throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name); 148 | } 149 | 150 | throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name); 151 | } 152 | 153 | public function hasProperty(string $name): bool 154 | { 155 | return isset($this->propertyValues[$name]) || in_array($name, $this->propertyNames(), true); 156 | } 157 | 158 | public function set(string $propertyName, mixed $value): void 159 | { 160 | if ($this->hasProperty($propertyName)) { 161 | parent::set($propertyName, $value); 162 | } else { 163 | throw new InvalidArgumentException(static::class . ' has no property named "' . $propertyName . '".'); 164 | } 165 | } 166 | 167 | /** 168 | * Returns a value indicating whether a property is defined for this component. 169 | * 170 | * A property is defined if: 171 | * 172 | * - the class has a getter or setter method associated with the specified name (in this case, property name is 173 | * case-insensitive). 174 | * - the class has a member variable with the specified name (when `$checkVars` is true). 175 | * 176 | * @param string $name The property name. 177 | * @param bool $checkVars Whether to treat member variables as properties. 178 | * 179 | * @return bool Whether the property is defined. 180 | * 181 | * {@see canGetProperty()} 182 | * {@see canSetProperty()} 183 | */ 184 | public function isProperty(string $name, bool $checkVars = true): bool 185 | { 186 | return method_exists($this, "get$name") 187 | || method_exists($this, "set$name") 188 | || method_exists($this, "get{$name}Query") 189 | || ($checkVars && property_exists($this, $name)) 190 | || $this->hasProperty($name); 191 | } 192 | 193 | public function canGetProperty(string $name, bool $checkVars = true): bool 194 | { 195 | return method_exists($this, "get$name") 196 | || method_exists($this, "get{$name}Query") 197 | || ($checkVars && property_exists($this, $name)) 198 | || $this->hasProperty($name); 199 | } 200 | 201 | public function canSetProperty(string $name, bool $checkVars = true): bool 202 | { 203 | return method_exists($this, "set$name") 204 | || ($checkVars && property_exists($this, $name)) 205 | || $this->hasProperty($name); 206 | } 207 | 208 | /** @psalm-return array */ 209 | protected function propertyValuesInternal(): array 210 | { 211 | return array_merge($this->propertyValues, parent::propertyValuesInternal()); 212 | } 213 | 214 | protected function populateProperty(string $name, mixed $value): void 215 | { 216 | if ($name !== 'propertyValues' && property_exists($this, $name)) { 217 | $this->$name = $value; 218 | } else { 219 | $this->propertyValues[$name] = $value; 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Trait/MagicRelationsTrait.php: -------------------------------------------------------------------------------- 1 | hasMany(Order::class, ['customer_id' => 'id']); 41 | * } 42 | * ``` 43 | * 44 | * @throws InvalidArgumentException If the named relation doesn't exist. 45 | * @throws ReflectionException 46 | */ 47 | public function relationQuery(string $name): ActiveQueryInterface 48 | { 49 | $getter = 'get' . ucfirst($name) . 'Query'; 50 | 51 | if (!method_exists($this, $getter)) { 52 | throw new InvalidArgumentException(static::class . ' has no relation named "' . $name . '".'); 53 | } 54 | 55 | $method = new ReflectionMethod($this, $getter); 56 | $type = $method->getReturnType(); 57 | 58 | if ( 59 | $type === null 60 | || !is_a('\\' . $type->getName(), ActiveQueryInterface::class, true) 61 | ) { 62 | $typeName = $type === null ? 'mixed' : $type->getName(); 63 | 64 | throw new InvalidArgumentException( 65 | 'Relation query method "' . static::class . '::' . $getter . '()" should return type "' 66 | . ActiveQueryInterface::class . '", but returns "' . $typeName . '" type.' 67 | ); 68 | } 69 | 70 | /** Relation name is case-sensitive, trying to validate it when the relation is defined within this class. */ 71 | $realName = lcfirst(substr($method->getName(), 3, -5)); 72 | 73 | if ($realName !== $name) { 74 | throw new InvalidArgumentException( 75 | 'Relation names are case sensitive. ' . static::class 76 | . " has a relation named \"$realName\" instead of \"$name\"." 77 | ); 78 | } 79 | 80 | return $this->$getter(); 81 | } 82 | 83 | /** 84 | * Returns names of all relations defined in the ActiveRecord class using getter methods with `get` prefix and 85 | * `Query` suffix. 86 | * 87 | * @throws ReflectionException 88 | * @return string[] 89 | */ 90 | public function relationNames(): array 91 | { 92 | $methods = get_class_methods($this); 93 | 94 | $relations = []; 95 | 96 | foreach ($methods as $method) { 97 | if (str_starts_with($method, 'get') && str_ends_with($method, 'Query')) { 98 | $reflection = new ReflectionMethod($this, $method); 99 | $type = $reflection->getReturnType(); 100 | 101 | if ( 102 | $type === null 103 | || !is_a('\\' . $type->getName(), ActiveQueryInterface::class, true) 104 | ) { 105 | continue; 106 | } 107 | 108 | $relations[] = lcfirst(substr($method, 3, -5)); 109 | } 110 | } 111 | 112 | return $relations; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/ArrayableTraitTest.php: -------------------------------------------------------------------------------- 1 | findByPk(1)->fields(); 19 | 20 | $this->assertEquals( 21 | [ 22 | 'id' => 'id', 23 | 'email' => 'email', 24 | 'name' => 'name', 25 | 'address' => 'address', 26 | 'status' => 'status', 27 | 'bool_status' => 'bool_status', 28 | 'profile_id' => 'profile_id', 29 | 'item' => 'item', 30 | 'items' => 'items', 31 | ], 32 | $fields, 33 | ); 34 | } 35 | 36 | public function testToArray(): void 37 | { 38 | $customerQuery = new ActiveQuery(Customer::class); 39 | $customer = $customerQuery->findByPk(1); 40 | 41 | $this->assertSame( 42 | [ 43 | 'id' => 1, 44 | 'email' => 'user1@example.com', 45 | 'name' => 'user1', 46 | 'address' => 'address1', 47 | 'status' => 1, 48 | 'bool_status' => true, 49 | 'profile_id' => 1, 50 | ], 51 | $customer->toArray(), 52 | ); 53 | } 54 | 55 | public function testToArrayWithClosure(): void 56 | { 57 | $customerQuery = new ActiveQuery(CustomerClosureField::class); 58 | $customer = $customerQuery->findByPk(1); 59 | 60 | $this->assertSame( 61 | [ 62 | 'id' => 1, 63 | 'email' => 'user1@example.com', 64 | 'name' => 'user1', 65 | 'address' => 'address1', 66 | 'status' => 'active', 67 | 'bool_status' => true, 68 | 'profile_id' => 1, 69 | ], 70 | $customer->toArray(), 71 | ); 72 | } 73 | 74 | public function testToArrayForArrayable(): void 75 | { 76 | $customerQuery = new ActiveQuery(CustomerForArrayable::class); 77 | 78 | /** @var CustomerForArrayable $customer */ 79 | $customer = $customerQuery->findByPk(1); 80 | /** @var CustomerForArrayable $customer2 */ 81 | $customer2 = $customerQuery->findByPk(2); 82 | /** @var CustomerForArrayable $customer3 */ 83 | $customer3 = $customerQuery->findByPk(3); 84 | 85 | $customer->setItem($customer2); 86 | $customer->setItems($customer3); 87 | 88 | $this->assertSame( 89 | [ 90 | 'id' => 1, 91 | 'email' => 'user1@example.com', 92 | 'name' => 'user1', 93 | 'address' => 'address1', 94 | 'status' => 'active', 95 | 'item' => [ 96 | 'id' => 2, 97 | 'email' => 'user2@example.com', 98 | 'name' => 'user2', 99 | 'status' => 'active', 100 | ], 101 | 'items' => [ 102 | [ 103 | 'id' => 3, 104 | 'email' => 'user3@example.com', 105 | 'name' => 'user3', 106 | 'status' => 'inactive', 107 | ], 108 | ], 109 | ], 110 | $customer->toArray([ 111 | 'id', 112 | 'name', 113 | 'email', 114 | 'address', 115 | 'status', 116 | 'item.id', 117 | 'item.name', 118 | 'item.email', 119 | 'items.0.id', 120 | 'items.0.name', 121 | 'items.0.email', 122 | ]), 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | orderBy('id'); 18 | 19 | $result = $query->batch(2); 20 | 21 | $this->assertEquals(2, $result->getBatchSize()); 22 | $this->assertSame($result->getQuery(), $query); 23 | 24 | /** normal query */ 25 | $customerQuery = new ActiveQuery(Customer::class); 26 | 27 | $query = $customerQuery->orderBy('id'); 28 | 29 | $allRows = []; 30 | 31 | $batch = $query->batch(2); 32 | 33 | foreach ($batch as $rows) { 34 | $allRows = array_merge($allRows, $rows); 35 | } 36 | 37 | $this->assertCount(3, $allRows); 38 | $this->assertEquals('user1', $allRows[0]->getName()); 39 | $this->assertEquals('user2', $allRows[1]->getName()); 40 | $this->assertEquals('user3', $allRows[2]->getName()); 41 | 42 | /** rewind */ 43 | $allRows = []; 44 | 45 | foreach ($batch as $rows) { 46 | $allRows = array_merge($allRows, $rows); 47 | } 48 | 49 | $this->assertCount(3, $allRows); 50 | 51 | /** rewind */ 52 | $batch->rewind(); 53 | 54 | /** empty query */ 55 | $query = $customerQuery->where(['id' => 100]); 56 | 57 | $allRows = []; 58 | 59 | $batch = $query->batch(2); 60 | 61 | foreach ($batch as $rows) { 62 | $allRows = array_merge($allRows, $rows); 63 | } 64 | 65 | $this->assertCount(0, $allRows); 66 | 67 | /** query with index */ 68 | $customerQuery = new ActiveQuery(Customer::class); 69 | 70 | $query = $customerQuery->indexBy('name'); 71 | 72 | $allRows = []; 73 | 74 | foreach ($query->batch(2) as $rows) { 75 | $allRows = array_merge($allRows, $rows); 76 | } 77 | 78 | $this->assertCount(3, $allRows); 79 | $this->assertEquals('address1', $allRows['user1']->getAddress()); 80 | $this->assertEquals('address2', $allRows['user2']->getAddress()); 81 | $this->assertEquals('address3', $allRows['user3']->getAddress()); 82 | 83 | /** each */ 84 | $customerQuery = new ActiveQuery(Customer::class); 85 | 86 | $query = $customerQuery->orderBy('id'); 87 | 88 | $allRows = []; 89 | 90 | foreach ($query->each() as $index => $row) { 91 | $allRows[$index] = $row; 92 | } 93 | $this->assertCount(3, $allRows); 94 | $this->assertEquals('user1', $allRows[0]->getName()); 95 | $this->assertEquals('user2', $allRows[1]->getName()); 96 | $this->assertEquals('user3', $allRows[2]->getName()); 97 | 98 | /** each with key */ 99 | $customerQuery = new ActiveQuery(Customer::class); 100 | 101 | $query = $customerQuery->orderBy('id')->indexBy('name'); 102 | 103 | $allRows = []; 104 | 105 | foreach ($query->each() as $key => $row) { 106 | $allRows[$key] = $row; 107 | } 108 | 109 | $this->assertCount(3, $allRows); 110 | $this->assertEquals('address1', $allRows['user1']->getAddress()); 111 | $this->assertEquals('address2', $allRows['user2']->getAddress()); 112 | $this->assertEquals('address3', $allRows['user3']->getAddress()); 113 | } 114 | 115 | public function testActiveQuery(): void 116 | { 117 | /** batch with eager loading */ 118 | $customerQuery = new ActiveQuery(Customer::class); 119 | 120 | $query = $customerQuery->with('orders')->orderBy('id'); 121 | 122 | $customers = $this->getAllRowsFromBatch($query->batch(2)); 123 | 124 | foreach ($customers as $customer) { 125 | $this->assertTrue($customer->isRelationPopulated('orders')); 126 | } 127 | 128 | $this->assertCount(3, $customers); 129 | $this->assertCount(1, $customers[0]->getOrders()); 130 | $this->assertCount(2, $customers[1]->getOrders()); 131 | $this->assertCount(0, $customers[2]->getOrders()); 132 | } 133 | 134 | public function testBatchWithIndexBy(): void 135 | { 136 | $customerQuery = new ActiveQuery(Customer::class); 137 | 138 | $query = $customerQuery->orderBy('id')->limit(3)->indexBy('id'); 139 | 140 | $customers = $this->getAllRowsFromBatch($query->batch(2)); 141 | 142 | $this->assertCount(3, $customers); 143 | $this->assertEquals('user1', $customers[0]->getName()); 144 | $this->assertEquals('user2', $customers[1]->getName()); 145 | $this->assertEquals('user3', $customers[2]->getName()); 146 | } 147 | 148 | protected function getAllRowsFromBatch(BatchQueryResultInterface $batch): array 149 | { 150 | $allRows = []; 151 | 152 | foreach ($batch as $rows) { 153 | $allRows = array_merge($allRows, $rows); 154 | } 155 | 156 | return $allRows; 157 | } 158 | 159 | protected function getAllRowsFromEach(BatchQueryResultInterface $each): array 160 | { 161 | $allRows = []; 162 | 163 | foreach ($each as $index => $row) { 164 | $allRows[$index] = $row; 165 | } 166 | 167 | return $allRows; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/ConnectionProviderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(ConnectionProvider::has('default')); 22 | $this->assertFalse(ConnectionProvider::has('db2')); 23 | 24 | $db = ConnectionProvider::get(); 25 | 26 | $this->assertTrue(ConnectionProvider::has('default')); 27 | $this->assertSame($db, ConnectionProvider::get('default')); 28 | 29 | $list = ConnectionProvider::all(); 30 | 31 | $this->assertSame($list, ['default' => $db]); 32 | 33 | $db2 = $this->createConnection(); 34 | ConnectionProvider::set($db2, 'db2'); 35 | 36 | $this->assertTrue(ConnectionProvider::has('db2')); 37 | $this->assertSame($db2, ConnectionProvider::get('db2')); 38 | 39 | $list = ConnectionProvider::all(); 40 | 41 | $this->assertSame($list, ['default' => $db, 'db2' => $db2]); 42 | 43 | ConnectionProvider::remove('db2'); 44 | 45 | $this->assertFalse(ConnectionProvider::has('db2')); 46 | 47 | $list = ConnectionProvider::all(); 48 | 49 | $this->assertSame($list, ['default' => $db]); 50 | } 51 | 52 | public function testConnectionProviderMiddleware(): void 53 | { 54 | $this->reloadFixtureAfterTest(); 55 | 56 | ConnectionProvider::remove(); 57 | 58 | $this->assertEmpty(ConnectionProvider::all()); 59 | $this->assertFalse(ConnectionProvider::has('default')); 60 | 61 | $db = $this->createConnection(); 62 | $container = new Container(ContainerConfig::create()->withDefinitions([ConnectionInterface::class => $db])); 63 | $request = $this->createMock(ServerRequestInterface::class); 64 | $requestHandler = $this->createMock(RequestHandlerInterface::class); 65 | 66 | $dispatcher = (new MiddlewareDispatcher(new MiddlewareFactory($container))) 67 | ->withMiddlewares([ConnectionProviderMiddleware::class]); 68 | 69 | $dispatcher->dispatch($request, $requestHandler); 70 | 71 | $this->assertTrue(ConnectionProvider::has('default')); 72 | $this->assertSame($db, ConnectionProvider::get()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/ActiveQueryFindTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/ActiveQueryTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/ActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 19 | } 20 | 21 | protected function createFactory(): Factory 22 | { 23 | return (new MssqlHelper())->createFactory($this->db()); 24 | } 25 | 26 | public function testSaveWithTrigger(): void 27 | { 28 | $this->reloadFixtureAfterTest(); 29 | 30 | // drop trigger if exist 31 | $sql = <<db()->createCommand($sql)->execute(); 38 | 39 | // create trigger 40 | $sql = <<db()->createCommand($sql)->execute(); 51 | 52 | $record = new TestTrigger(); 53 | 54 | $record->stringcol = 'test'; 55 | 56 | $this->assertTrue($record->save()); 57 | $this->assertEquals(1, $record->id); 58 | 59 | $testRecordQuery = new ActiveQuery(TestTriggerAlert::class); 60 | 61 | $this->assertEquals('test', $testRecordQuery->findByPk(1)->stringcol); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/ArrayableTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/ConnectionProviderTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/MagicActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 18 | } 19 | 20 | public function testSaveWithTrigger(): void 21 | { 22 | $this->reloadFixtureAfterTest(); 23 | 24 | // drop trigger if exist 25 | $sql = <<db()->createCommand($sql)->execute(); 32 | 33 | // create trigger 34 | $sql = <<db()->createCommand($sql)->execute(); 45 | 46 | $record = new TestTrigger(); 47 | 48 | $record->stringcol = 'test'; 49 | 50 | $this->assertTrue($record->save()); 51 | $this->assertEquals(1, $record->id); 52 | 53 | $testRecordQuery = new ActiveQuery(TestTriggerAlert::class); 54 | 55 | $this->assertEquals('test', $testRecordQuery->findByPk(1)->stringcol); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Driver/Mssql/RepositoryTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/ActiveQueryFindTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/ActiveQueryTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/ActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 20 | } 21 | 22 | protected function createFactory(): Factory 23 | { 24 | return (new MysqlHelper())->createFactory($this->db()); 25 | } 26 | 27 | public function testCastValues(): void 28 | { 29 | $this->reloadFixtureAfterTest(); 30 | 31 | $arClass = new Type(); 32 | 33 | $arClass->int_col = 123; 34 | $arClass->int_col2 = 456; 35 | $arClass->smallint_col = 42; 36 | $arClass->char_col = '1337'; 37 | $arClass->char_col2 = 'test'; 38 | $arClass->char_col3 = 'test123'; 39 | $arClass->enum_col = 'B'; 40 | $arClass->float_col = 3.742; 41 | $arClass->float_col2 = 42.1337; 42 | $arClass->bool_col = true; 43 | $arClass->bool_col2 = false; 44 | $arClass->json_col = ['a' => 'b', 'c' => null, 'd' => [1, 2, 3]]; 45 | 46 | $arClass->save(); 47 | 48 | /** @var $model Type */ 49 | $aqClass = new ActiveQuery(Type::class); 50 | $query = $aqClass->one(); 51 | 52 | $this->assertSame(123, $query->int_col); 53 | $this->assertSame(456, $query->int_col2); 54 | $this->assertSame(42, $query->smallint_col); 55 | $this->assertSame('1337', trim($query->char_col)); 56 | $this->assertSame('test', $query->char_col2); 57 | $this->assertSame('test123', $query->char_col3); 58 | $this->assertSame(3.742, $query->float_col); 59 | $this->assertSame(42.1337, $query->float_col2); 60 | $this->assertEquals(true, $query->bool_col); 61 | $this->assertEquals(false, $query->bool_col2); 62 | $this->assertSame('B', $query->enum_col); 63 | $this->assertSame(['a' => 'b', 'c' => null, 'd' => [1, 2, 3]], $query->json_col); 64 | } 65 | 66 | public function testExplicitPkOnAutoIncrement(): void 67 | { 68 | $this->reloadFixtureAfterTest(); 69 | 70 | $customer = new Customer(); 71 | 72 | $customer->setId(1337); 73 | $customer->setEmail('user1337@example.com'); 74 | $customer->setName('user1337'); 75 | $customer->setAddress('address1337'); 76 | 77 | $this->assertTrue($customer->isNewRecord()); 78 | 79 | $customer->save(); 80 | 81 | $this->assertEquals(1337, $customer->getId()); 82 | $this->assertFalse($customer->isNewRecord()); 83 | } 84 | 85 | /** 86 | * {@see https://github.com/yiisoft/yii2/issues/15482} 87 | */ 88 | public function testEagerLoadingUsingStringIdentifiers(): void 89 | { 90 | $betaQuery = new ActiveQuery(Beta::class); 91 | 92 | $betas = $betaQuery->with('alpha')->all(); 93 | 94 | $this->assertNotEmpty($betas); 95 | 96 | $alphaIdentifiers = []; 97 | 98 | /** @var Beta[] $betas */ 99 | foreach ($betas as $beta) { 100 | $this->assertNotNull($beta->getAlpha()); 101 | $this->assertEquals($beta->getAlphaStringIdentifier(), $beta->getAlpha()->getStringIdentifier()); 102 | $alphaIdentifiers[] = $beta->getAlpha()->getStringIdentifier(); 103 | } 104 | 105 | $this->assertEquals(['1', '01', '001', '001', '2', '2b', '2b', '02'], $alphaIdentifiers); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/ArrayableTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/ConnectionProviderTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/MagicActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 18 | } 19 | 20 | public function testExplicitPkOnAutoIncrement(): void 21 | { 22 | $this->reloadFixtureAfterTest(); 23 | 24 | $customer = new Customer(); 25 | 26 | $customer->id = 1337; 27 | $customer->email = 'user1337@example.com'; 28 | $customer->name = 'user1337'; 29 | $customer->address = 'address1337'; 30 | 31 | $this->assertTrue($customer->isNewRecord()); 32 | 33 | $customer->save(); 34 | 35 | $this->assertEquals(1337, $customer->id); 36 | $this->assertFalse($customer->isNewRecord()); 37 | } 38 | 39 | /** 40 | * {@see https://github.com/yiisoft/yii2/issues/15482} 41 | */ 42 | public function testEagerLoadingUsingStringIdentifiers(): void 43 | { 44 | $betaQuery = new ActiveQuery(Beta::class); 45 | 46 | $betas = $betaQuery->with('alpha')->all(); 47 | 48 | $this->assertNotEmpty($betas); 49 | 50 | $alphaIdentifiers = []; 51 | 52 | /** @var Beta[] $betas */ 53 | foreach ($betas as $beta) { 54 | $this->assertNotNull($beta->alpha); 55 | $this->assertEquals($beta->alpha_string_identifier, $beta->alpha->string_identifier); 56 | $alphaIdentifiers[] = $beta->alpha->string_identifier; 57 | } 58 | 59 | $this->assertEquals(['1', '01', '001', '001', '2', '2b', '2b', '02'], $alphaIdentifiers); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/RepositoryTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Mysql/Stubs/Type.php: -------------------------------------------------------------------------------- 1 | createConnection(); 17 | } 18 | 19 | public function testFindLimit(): void 20 | { 21 | /** one */ 22 | $customerQuery = new ActiveQuery(CustomerWithRownumid::class); 23 | $customer = $customerQuery->orderBy('id')->one(); 24 | $this->assertEquals('user1', $customer->getName()); 25 | 26 | /** all */ 27 | $customerQuery = new ActiveQuery(CustomerWithRownumid::class); 28 | $customers = $customerQuery->all(); 29 | $this->assertCount(3, $customers); 30 | 31 | /** limit */ 32 | $customerQuery = new ActiveQuery(CustomerWithRownumid::class); 33 | $customers = $customerQuery->orderBy('id')->limit(1)->all(); 34 | $this->assertCount(1, $customers); 35 | $this->assertEquals('user1', $customers[0]->getName()); 36 | 37 | $customers = $customerQuery->orderBy('id')->limit(1)->offset(1)->all(); 38 | $this->assertCount(1, $customers); 39 | $this->assertEquals('user2', $customers[0]->getName()); 40 | 41 | $customers = $customerQuery->orderBy('id')->limit(1)->offset(2)->all(); 42 | $this->assertCount(1, $customers); 43 | $this->assertEquals('user3', $customers[0]->getName()); 44 | 45 | $customers = $customerQuery->orderBy('id')->limit(2)->offset(1)->all(); 46 | $this->assertCount(2, $customers); 47 | $this->assertEquals('user2', $customers[0]->getName()); 48 | $this->assertEquals('user3', $customers[1]->getName()); 49 | 50 | $customers = $customerQuery->limit(2)->offset(3)->all(); 51 | $this->assertCount(0, $customers); 52 | 53 | /** offset */ 54 | $customerQuery = new ActiveQuery(CustomerWithRownumid::class); 55 | $customer = $customerQuery->orderBy('id')->offset(0)->one(); 56 | $this->assertEquals('user1', $customer->getName()); 57 | 58 | $customer = $customerQuery->orderBy('id')->offset(1)->one(); 59 | $this->assertEquals('user2', $customer->getName()); 60 | 61 | $customer = $customerQuery->orderBy('id')->offset(2)->one(); 62 | $this->assertEquals('user3', $customer->getName()); 63 | 64 | $customer = $customerQuery->offset(3)->one(); 65 | $this->assertNull($customer); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/ActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 19 | } 20 | 21 | protected function createFactory(): Factory 22 | { 23 | return (new OracleHelper())->createFactory($this->db()); 24 | } 25 | 26 | public function testDefaultValues(): void 27 | { 28 | $arClass = new Type(); 29 | $arClass->loadDefaultValues(); 30 | $this->assertSame(1, $arClass->int_col2); 31 | $this->assertSame('something', $arClass->char_col2); 32 | $this->assertSame(1.23, $arClass->float_col2); 33 | $this->assertSame(33.22, $arClass->numeric_col); 34 | $this->assertTrue($arClass->bool_col2); 35 | 36 | // not testing $arClass->time, because oci\Schema can't read default value 37 | 38 | $arClass = new Type(); 39 | $arClass->char_col2 = 'not something'; 40 | 41 | $arClass->loadDefaultValues(); 42 | $this->assertSame('not something', $arClass->char_col2); 43 | 44 | $arClass = new Type(); 45 | $arClass->char_col2 = 'not something'; 46 | 47 | $arClass->loadDefaultValues(false); 48 | $this->assertSame('something', $arClass->char_col2); 49 | } 50 | 51 | /** 52 | * Some PDO implementations (e.g. cubrid) do not support boolean values. 53 | * 54 | * Make sure this does not affect AR layer. 55 | */ 56 | public function testBooleanProperty(): void 57 | { 58 | $this->reloadFixtureAfterTest(); 59 | 60 | $customer = new Customer(); 61 | 62 | $customer->setName('boolean customer'); 63 | $customer->setEmail('mail@example.com'); 64 | $customer->setBoolStatus(true); 65 | 66 | $customer->save(); 67 | $customer->refresh(); 68 | $this->assertTrue($customer->getBoolStatus()); 69 | 70 | $customer->setBoolStatus(false); 71 | $customer->save(); 72 | 73 | $customer->refresh(); 74 | $this->assertFalse($customer->getBoolStatus()); 75 | 76 | $customerQuery = new ActiveQuery(Customer::class); 77 | $customers = $customerQuery->where(['bool_status' => '1'])->all(); 78 | $this->assertCount(2, $customers); 79 | 80 | $customerQuery = new ActiveQuery(Customer::class); 81 | $customers = $customerQuery->where(['bool_status' => '0'])->all(); 82 | $this->assertCount(2, $customers); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/ArrayableTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 17 | } 18 | 19 | public function testBatchWithIndexBy(): void 20 | { 21 | $customerQuery = new ActiveQuery(Customer::class); 22 | 23 | $query = $customerQuery->orderBy('id')->limit(3)->indexBy('id'); 24 | 25 | $customers = $this->getAllRowsFromBatch($query->batch(2)); 26 | 27 | $this->assertCount(3, $customers); 28 | $this->assertEquals('user1', $customers[0]->getName()); 29 | $this->assertEquals('user2', $customers[1]->getName()); 30 | $this->assertEquals('user3', $customers[2]->getName()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/ConnectionProviderTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/MagicActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 18 | } 19 | 20 | public function testDefaultValues(): void 21 | { 22 | $arClass = new Type(); 23 | $arClass->loadDefaultValues(); 24 | $this->assertSame(1, $arClass->int_col2); 25 | $this->assertSame('something', $arClass->char_col2); 26 | $this->assertSame(1.23, $arClass->float_col2); 27 | $this->assertSame(33.22, $arClass->numeric_col); 28 | $this->assertSame(true, $arClass->bool_col2); 29 | 30 | // not testing $arClass->time, because oci\Schema can't read default value 31 | 32 | $arClass = new Type(); 33 | $arClass->char_col2 = 'not something'; 34 | 35 | $arClass->loadDefaultValues(); 36 | $this->assertSame('not something', $arClass->char_col2); 37 | 38 | $arClass = new Type(); 39 | $arClass->char_col2 = 'not something'; 40 | 41 | $arClass->loadDefaultValues(false); 42 | $this->assertSame('something', $arClass->char_col2); 43 | } 44 | 45 | /** 46 | * Some PDO implementations (e.g. cubrid) do not support boolean values. 47 | * 48 | * Make sure this does not affect AR layer. 49 | */ 50 | public function testBooleanProperty(): void 51 | { 52 | $this->reloadFixtureAfterTest(); 53 | 54 | $customer = new Customer(); 55 | 56 | $customer->name = 'boolean customer'; 57 | $customer->email = 'mail@example.com'; 58 | $customer->bool_status = true; 59 | 60 | $customer->save(); 61 | $customer->refresh(); 62 | $this->assertTrue($customer->bool_status); 63 | 64 | $customer->bool_status = false; 65 | $customer->save(); 66 | 67 | $customer->refresh(); 68 | $this->assertFalse($customer->bool_status); 69 | 70 | $customerQuery = new ActiveQuery(Customer::class); 71 | $customers = $customerQuery->where(['bool_status' => '1'])->all(); 72 | $this->assertCount(2, $customers); 73 | 74 | $customerQuery = new ActiveQuery(Customer::class); 75 | $customers = $customerQuery->where(['bool_status' => '0'])->all(); 76 | $this->assertCount(2, $customers); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/RepositoryTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/Stubs/Customer.php: -------------------------------------------------------------------------------- 1 | hasMany(Order::class, ['customer_id' => 'id'])->orderBy('{{customer}}.[[id]]'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/Stubs/MagicCustomer.php: -------------------------------------------------------------------------------- 1 | hasMany(Order::class, ['customer_id' => 'id'])->orderBy('{{customer}}.[[id]]'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/Stubs/MagicOrder.php: -------------------------------------------------------------------------------- 1 | hasOne(MagicCustomer::class, ['id' => 'customer_id']); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Driver/Oracle/Stubs/Order.php: -------------------------------------------------------------------------------- 1 | hasOne(Customer::class, ['id' => 'customer_id']); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/ActiveQueryFindTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/ActiveQueryTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 17 | } 18 | 19 | public function testBit(): void 20 | { 21 | $bitValueQuery = new ActiveQuery(BitValues::class); 22 | $falseBit = $bitValueQuery->findByPk(1); 23 | $this->assertSame(0, $falseBit->val); 24 | 25 | $bitValueQuery = new ActiveQuery(BitValues::class); 26 | $trueBit = $bitValueQuery->findByPk(2); 27 | $this->assertSame(1, $trueBit->val); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/ArrayableTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/ConnectionProviderTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/RepositoryTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/Stubs/Item.php: -------------------------------------------------------------------------------- 1 | $this->hasMany(Promotion::class, ['array_item_ids' => 'id']), 18 | default => parent::relationQuery($name), 19 | }; 20 | } 21 | 22 | /** @return Promotion[] */ 23 | public function getPromotionsViaArray(): array 24 | { 25 | return $this->relation('promotionsViaArray'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/Stubs/Promotion.php: -------------------------------------------------------------------------------- 1 | $this->hasMany(Item::class, ['id' => 'array_item_ids']) 20 | ->inverseOf('promotionsViaArray'), 21 | default => parent::relationQuery($name), 22 | }; 23 | } 24 | 25 | /** @return Item[] */ 26 | public function getItemsViaArray(): array 27 | { 28 | return $this->relation('itemsViaArray'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Driver/Pgsql/Stubs/Type.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/ActiveQueryTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/ActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 19 | } 20 | 21 | protected function createFactory(): Factory 22 | { 23 | return (new SqliteHelper())->createFactory($this->db()); 24 | } 25 | 26 | public function testExplicitPkOnAutoIncrement(): void 27 | { 28 | $this->reloadFixtureAfterTest(); 29 | 30 | $customer = new Customer(); 31 | 32 | $customer->setId(1337); 33 | $customer->setEmail('user1337@example.com'); 34 | $customer->setName('user1337'); 35 | $customer->setAddress('address1337'); 36 | 37 | $this->assertTrue($customer->isNewRecord()); 38 | $customer->save(); 39 | 40 | $this->assertEquals(1337, $customer->getId()); 41 | $this->assertFalse($customer->isNewRecord()); 42 | } 43 | 44 | /** 45 | * {@see https://github.com/yiisoft/yii2/issues/15482} 46 | */ 47 | public function testEagerLoadingUsingStringIdentifiers(): void 48 | { 49 | $betaQuery = new ActiveQuery(Beta::class); 50 | 51 | $betas = $betaQuery->with('alpha')->all(); 52 | 53 | $this->assertNotEmpty($betas); 54 | 55 | $alphaIdentifiers = []; 56 | 57 | /** @var Beta[] $betas */ 58 | foreach ($betas as $beta) { 59 | $this->assertNotNull($beta->getAlpha()); 60 | $this->assertEquals($beta->getAlphaStringIdentifier(), $beta->getAlpha()->getStringIdentifier()); 61 | $alphaIdentifiers[] = $beta->getAlpha()->getStringIdentifier(); 62 | } 63 | 64 | $this->assertEquals(['1', '01', '001', '001', '2', '2b', '2b', '02'], $alphaIdentifiers); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/ArrayableTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/BatchQueryResultTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/ConnectionProviderTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/MagicActiveRecordTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 18 | } 19 | 20 | public function testExplicitPkOnAutoIncrement(): void 21 | { 22 | $this->reloadFixtureAfterTest(); 23 | 24 | $customer = new Customer(); 25 | 26 | $customer->id = 1337; 27 | $customer->email = 'user1337@example.com'; 28 | $customer->name = 'user1337'; 29 | $customer->address = 'address1337'; 30 | 31 | $this->assertTrue($customer->isNewRecord()); 32 | $customer->save(); 33 | 34 | $this->assertEquals(1337, $customer->id); 35 | $this->assertFalse($customer->isNewRecord()); 36 | } 37 | 38 | /** 39 | * {@see https://github.com/yiisoft/yii2/issues/15482} 40 | */ 41 | public function testEagerLoadingUsingStringIdentifiers(): void 42 | { 43 | $betaQuery = new ActiveQuery(Beta::class); 44 | 45 | $betas = $betaQuery->with('alpha')->all(); 46 | 47 | $this->assertNotEmpty($betas); 48 | 49 | $alphaIdentifiers = []; 50 | 51 | /** @var Beta[] $betas */ 52 | foreach ($betas as $beta) { 53 | $this->assertNotNull($beta->alpha); 54 | $this->assertEquals($beta->alpha_string_identifier, $beta->alpha->string_identifier); 55 | $alphaIdentifiers[] = $beta->alpha->string_identifier; 56 | } 57 | 58 | $this->assertEquals(['1', '01', '001', '001', '2', '2b', '2b', '02'], $alphaIdentifiers); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Driver/Sqlite/RepositoryTraitTest.php: -------------------------------------------------------------------------------- 1 | createConnection(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/RepositoryTraitTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 18 | $customerQuery->setWhere(['id' => 1]), 19 | Customer::find(['id' => 1]), 20 | ); 21 | } 22 | 23 | public function testFindOne(): void 24 | { 25 | $customerQuery = new ActiveQuery(new Customer()); 26 | 27 | $this->assertEquals( 28 | $customerQuery->where(['id' => 1])->one(), 29 | Customer::findOne(['id' => 1]), 30 | ); 31 | 32 | $customer = Customer::findOne(['customer.id' => 1]); 33 | $this->assertEquals(1, $customer->getId()); 34 | 35 | $customer = Customer::findOne(['id' => [5, 6, 1]]); 36 | $this->assertInstanceOf(Customer::class, $customer); 37 | 38 | $customer = Customer::findOne(['id' => 2, 'name' => 'user2']); 39 | $this->assertInstanceOf(Customer::class, $customer); 40 | $this->assertEquals('user2', $customer->getName()); 41 | 42 | $customer = Customer::findOne(['id' => 2, 'name' => 'user1']); 43 | $this->assertNull($customer); 44 | 45 | $customer = Customer::findOne(['name' => 'user5']); 46 | $this->assertNull($customer); 47 | } 48 | 49 | public function testFindOneOrFail(): void 50 | { 51 | $customerQuery = new ActiveQuery(new Customer()); 52 | 53 | $this->assertEquals( 54 | $customerQuery->where(['id' => 1])->one(), 55 | Customer::findOneOrFail(['id' => 1]), 56 | ); 57 | 58 | $this->expectException(NotFoundException::class); 59 | $this->expectExceptionMessage('No records found.'); 60 | 61 | Customer::findOneOrFail(['name' => 'user5']); 62 | } 63 | 64 | public function testFindAll(): void 65 | { 66 | $customerQuery = new ActiveQuery(new Customer()); 67 | 68 | $this->assertEquals( 69 | $customerQuery->all(), 70 | Customer::findAll(), 71 | ); 72 | 73 | $this->assertEquals( 74 | $customerQuery->where(['id' => 1])->all(), 75 | Customer::findAll(['id' => 1]), 76 | ); 77 | 78 | $this->assertCount(1, Customer::findAll(['id' => 1])); 79 | $this->assertCount(3, Customer::findAll(['id' => [1, 2, 3]])); 80 | } 81 | 82 | public function testFindAllOrFail(): void 83 | { 84 | $customerQuery = new ActiveQuery(new Customer()); 85 | 86 | $this->assertEquals( 87 | $customerQuery->where(['id' => [1, 2, 3]])->all(), 88 | Customer::findAllOrFail(['id' => [1, 2, 3]]), 89 | ); 90 | 91 | $this->expectException(NotFoundException::class); 92 | $this->expectExceptionMessage('No records found.'); 93 | 94 | Customer::findAllOrFail(['id' => 5]); 95 | } 96 | 97 | public function testFindByPk(): void 98 | { 99 | $customerQuery = new ActiveQuery(new Customer()); 100 | 101 | $this->assertEquals( 102 | $customerQuery->where(['id' => 1])->one(), 103 | Customer::findByPk(1), 104 | ); 105 | 106 | $customer = Customer::findByPk(5); 107 | $this->assertNull($customer); 108 | } 109 | 110 | public function testFindByPkOrFail(): void 111 | { 112 | $customerQuery = new ActiveQuery(new Customer()); 113 | 114 | $this->assertEquals( 115 | $customerQuery->where(['id' => 1])->one(), 116 | Customer::findByPkOrFail(1), 117 | ); 118 | 119 | $this->expectException(NotFoundException::class); 120 | $this->expectExceptionMessage('No records found.'); 121 | 122 | Customer::findByPkOrFail(5); 123 | } 124 | 125 | public function testFindBySql(): void 126 | { 127 | $customerQuery = new ActiveQuery(new Customer()); 128 | 129 | $this->assertEquals( 130 | $customerQuery->sql('SELECT * FROM {{customer}}'), 131 | Customer::findBySql('SELECT * FROM {{customer}}'), 132 | ); 133 | 134 | $customer = Customer::findBySql('SELECT * FROM {{customer}} ORDER BY [[id]] DESC')->one(); 135 | $this->assertInstanceOf(Customer::class, $customer); 136 | $this->assertSame('user3', $customer->get('name')); 137 | 138 | $customers = Customer::findBySql('SELECT * FROM {{customer}}')->all(); 139 | $this->assertCount(3, $customers); 140 | 141 | /** find with parameter binding */ 142 | $customer = Customer::findBySql('SELECT * FROM {{customer}} WHERE [[id]]=:id', [':id' => 2])->one(); 143 | $this->assertInstanceOf(Customer::class, $customer); 144 | $this->assertSame('user2', $customer->get('name')); 145 | 146 | /** @link https://github.com/yiisoft/yii2/issues/8593 */ 147 | $query = Customer::findBySql('SELECT * FROM {{customer}}'); 148 | $this->assertEquals(3, $query->count()); 149 | 150 | $query = Customer::findBySql('SELECT * FROM {{customer}} WHERE [[id]]=:id', [':id' => 2]); 151 | $this->assertEquals(1, $query->count()); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Alpha.php: -------------------------------------------------------------------------------- 1 | id; 27 | } 28 | 29 | public function getStringIdentifier(): string 30 | { 31 | return $this->string_identifier; 32 | } 33 | 34 | public function relationQuery(string $name): ActiveQueryInterface 35 | { 36 | return match ($name) { 37 | 'betas' => $this->getBetasQuery(), 38 | default => parent::relationQuery($name), 39 | }; 40 | } 41 | 42 | public function getBetas(): array|null 43 | { 44 | return $this->relation('betas'); 45 | } 46 | 47 | public function getBetasQuery(): ActiveQuery 48 | { 49 | return $this->hasMany(Beta::class, ['alpha_string_identifier' => 'string_identifier']); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Animal.php: -------------------------------------------------------------------------------- 1 | type = static::class; 27 | } 28 | 29 | public function getDoes() 30 | { 31 | return $this->does; 32 | } 33 | 34 | public function setDoes(string $value): void 35 | { 36 | $this->does = $value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/ArrayAndJsonTypes.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | public function getAlphaStringIdentifier(): string 28 | { 29 | return $this->alpha_string_identifier; 30 | } 31 | 32 | public function relationQuery(string $name): ActiveQueryInterface 33 | { 34 | return match ($name) { 35 | 'alpha' => $this->getAlphaQuery(), 36 | default => parent::relationQuery($name), 37 | }; 38 | } 39 | 40 | public function getAlpha(): Alpha|null 41 | { 42 | return $this->relation('alpha'); 43 | } 44 | 45 | public function getAlphaQuery(): ActiveQuery 46 | { 47 | return $this->hasOne(Alpha::class, ['string_identifier' => 'alpha_string_identifier']); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/BitValues.php: -------------------------------------------------------------------------------- 1 | setDoes('meow'); 16 | } 17 | 18 | public function getException(): void 19 | { 20 | throw new Exception('no'); 21 | } 22 | 23 | /** 24 | * This is to test if __isset catches the error. 25 | * 26 | * @throw DivisionByZeroError 27 | */ 28 | public function getThrowable(): float|int 29 | { 30 | return 5 / 0; 31 | } 32 | 33 | public function setNonExistingProperty(string $value): void 34 | { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Category.php: -------------------------------------------------------------------------------- 1 | $this->getItemsQuery(), 28 | 'limitedItems' => $this->getLimitedItemsQuery(), 29 | 'orderItems' => $this->getOrderItemsQuery(), 30 | 'orders' => $this->getOrdersQuery(), 31 | default => parent::relationQuery($name), 32 | }; 33 | } 34 | 35 | public function getId(): int|null 36 | { 37 | return $this->id; 38 | } 39 | 40 | public function getName(): string 41 | { 42 | return $this->name; 43 | } 44 | 45 | public function setId(int|null $id): void 46 | { 47 | $this->set('id', $id); 48 | } 49 | 50 | public function getLimitedItems(): array 51 | { 52 | return $this->relation('limitedItems'); 53 | } 54 | 55 | public function getLimitedItemsQuery(): ActiveQuery 56 | { 57 | return $this->hasMany(Item::class, ['category_id' => 'id'])->onCondition(['item.id' => [1, 2, 3]]); 58 | } 59 | 60 | public function getItems(): array 61 | { 62 | return $this->relation('items'); 63 | } 64 | 65 | public function getItemsQuery(): ActiveQuery 66 | { 67 | return $this->hasMany(Item::class, ['category_id' => 'id']); 68 | } 69 | 70 | public function getOrderItems(): array 71 | { 72 | return $this->relation('orderItems'); 73 | } 74 | 75 | public function getOrderItemsQuery(): ActiveQuery 76 | { 77 | return $this->hasMany(OrderItem::class, ['item_id' => 'id'])->via('items'); 78 | } 79 | 80 | public function getOrders(): array 81 | { 82 | return $this->relation('orders'); 83 | } 84 | 85 | public function getOrdersQuery(): ActiveQuery 86 | { 87 | return $this->hasMany(Order::class, ['id' => 'order_id'])->via('orderItems'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/CustomerClosureField.php: -------------------------------------------------------------------------------- 1 | $customer->status === 1 ? 'active' : 'inactive'; 32 | 33 | return $fields; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/CustomerForArrayable.php: -------------------------------------------------------------------------------- 1 | item = $item; 44 | } 45 | 46 | public function setItems(self ...$items) 47 | { 48 | $this->items = $items; 49 | } 50 | 51 | public function toArray(array $fields = [], array $expand = [], bool $recursive = true): array 52 | { 53 | $data = parent::toArray($fields, $expand, $recursive); 54 | 55 | $data['status'] = $this->status == 1 ? 'active' : 'inactive'; 56 | 57 | return $data; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/CustomerQuery.php: -------------------------------------------------------------------------------- 1 | andWhere('[[status]]=1'); 16 | 17 | return $this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/CustomerWithAlias.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 19 | } 20 | 21 | public function relationQuery(string $name): ActiveQueryInterface 22 | { 23 | return match ($name) { 24 | 'ordersWithFactory' => $this->hasMany(OrderWithFactory::class, ['customer_id' => 'id']), 25 | default => parent::relationQuery($name), 26 | }; 27 | } 28 | 29 | /** @return OrderWithFactory[] */ 30 | public function getOrdersWithFactory(): array 31 | { 32 | return $this->relation('ordersWithFactory'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/DefaultPk.php: -------------------------------------------------------------------------------- 1 | $this->getEmployeesQuery(), 29 | default => parent::relationQuery($name), 30 | }; 31 | } 32 | 33 | public function getEmployees(): ActiveRecordInterface 34 | { 35 | return $this->relation('employees'); 36 | } 37 | 38 | public function getEmployeesQuery(): ActiveQuery 39 | { 40 | return $this->hasMany( 41 | Employee::class, 42 | [ 43 | 'department_id' => 'id', 44 | ] 45 | )->inverseOf('department'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Document.php: -------------------------------------------------------------------------------- 1 | setDoes('bark'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Dossier.php: -------------------------------------------------------------------------------- 1 | id; 29 | } 30 | 31 | public function getDepartmentId(): int 32 | { 33 | return $this->department_id; 34 | } 35 | 36 | public function getEmployeeId(): int 37 | { 38 | return $this->employee_id; 39 | } 40 | 41 | public function getSummary(): string 42 | { 43 | return $this->summary; 44 | } 45 | 46 | public function setId(int $id): void 47 | { 48 | $this->id = $id; 49 | } 50 | 51 | public function setDepartmentId(int $departmentId): void 52 | { 53 | $this->set('department_id', $departmentId); 54 | } 55 | 56 | public function setEmployeeId(int $employeeId): void 57 | { 58 | $this->set('employee_id', $employeeId); 59 | } 60 | 61 | public function setSummary(string $summary): void 62 | { 63 | $this->summary = $summary; 64 | } 65 | 66 | public function relationQuery(string $name): ActiveQueryInterface 67 | { 68 | return match ($name) { 69 | 'employee' => $this->getEmployeeQuery(), 70 | default => parent::relationQuery($name), 71 | }; 72 | } 73 | 74 | public function getEmployee(): Employee|null 75 | { 76 | return $this->relation('employee'); 77 | } 78 | 79 | public function getEmployeeQuery(): ActiveQuery 80 | { 81 | return $this->hasOne( 82 | Employee::class, 83 | [ 84 | 'department_id' => 'department_id', 85 | 'id' => 'employee_id', 86 | ] 87 | )->inverseOf('dossier'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Employee.php: -------------------------------------------------------------------------------- 1 | $this->getDepartmentQuery(), 30 | 'dossier' => $this->getDossierQuery(), 31 | default => parent::relationQuery($name), 32 | }; 33 | } 34 | 35 | public function getFullName(): string 36 | { 37 | return $this->first_name . ' ' . $this->last_name; 38 | } 39 | 40 | public function getDepartment(): Department 41 | { 42 | return $this->relation('department'); 43 | } 44 | 45 | public function getDepartmentQuery(): ActiveQuery 46 | { 47 | return $this 48 | ->hasOne(Department::class, [ 49 | 'id' => 'department_id', 50 | ]) 51 | ->inverseOf('employees') 52 | ; 53 | } 54 | 55 | public function getDossier(): Dossier 56 | { 57 | return $this->relation('dossier'); 58 | } 59 | 60 | public function getDossierQuery(): ActiveQuery 61 | { 62 | return $this->hasOne( 63 | Dossier::class, 64 | [ 65 | 'department_id' => 'department_id', 66 | 'employee_id' => 'id', 67 | ] 68 | )->inverseOf('employee'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Item.php: -------------------------------------------------------------------------------- 1 | $this->getCategoryQuery(), 29 | 'promotionsViaJson' => $this->hasMany(Promotion::class, ['json_item_ids' => 'id']), 30 | default => parent::relationQuery($name), 31 | }; 32 | } 33 | 34 | public function getId(): int 35 | { 36 | return $this->id; 37 | } 38 | 39 | public function getName(): string 40 | { 41 | return $this->name; 42 | } 43 | 44 | public function getCategoryId(): int 45 | { 46 | return $this->category_id; 47 | } 48 | 49 | public function getCategory(): Category 50 | { 51 | return $this->relation('category'); 52 | } 53 | 54 | public function getCategoryQuery(): ActiveQuery 55 | { 56 | return $this->hasOne(Category::class, ['id' => 'category_id']); 57 | } 58 | 59 | /** @return Promotion[] */ 60 | public function getPromotionsViaJson(): array 61 | { 62 | return $this->relation('promotionsViaJson'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/NoExist.php: -------------------------------------------------------------------------------- 1 | tableName ??= 'order_item'; 27 | } 28 | 29 | public function getOrderId(): int 30 | { 31 | return $this->order_id; 32 | } 33 | 34 | public function getItemId(): int 35 | { 36 | return $this->item_id; 37 | } 38 | 39 | public function getQuantity(): int 40 | { 41 | return $this->quantity; 42 | } 43 | 44 | public function getSubtotal(): float 45 | { 46 | return $this->subtotal; 47 | } 48 | 49 | public function setOrderId(int $orderId): void 50 | { 51 | $this->set('order_id', $orderId); 52 | } 53 | 54 | public function setItemId(int $itemId): void 55 | { 56 | $this->set('item_id', $itemId); 57 | } 58 | 59 | public function setQuantity(int $quantity): void 60 | { 61 | $this->quantity = $quantity; 62 | } 63 | 64 | public function setSubtotal(float $subtotal): void 65 | { 66 | $this->subtotal = $subtotal; 67 | } 68 | 69 | public function relationQuery(string $name): ActiveQueryInterface 70 | { 71 | return match ($name) { 72 | 'order' => $this->getOrderQuery(), 73 | 'item' => $this->getItemQuery(), 74 | 'orderItemCompositeWithJoin' => $this->getOrderItemCompositeWithJoinQuery(), 75 | 'orderItemCompositeNoJoin' => $this->getOrderItemCompositeNoJoinQuery(), 76 | 'custom' => $this->getCustomQuery(), 77 | default => parent::relationQuery($name), 78 | }; 79 | } 80 | 81 | public function getOrder(): Order|null 82 | { 83 | return $this->relation('order'); 84 | } 85 | 86 | public function getOrderQuery(): ActiveQuery 87 | { 88 | return $this->hasOne(Order::class, ['id' => 'order_id']); 89 | } 90 | 91 | public function getItem(): Item|null 92 | { 93 | return $this->relation('item'); 94 | } 95 | 96 | public function getItemQuery(): ActiveQuery 97 | { 98 | return $this->hasOne(Item::class, ['id' => 'item_id']); 99 | } 100 | 101 | public function getOrderItemCompositeWithJoin(): self|null 102 | { 103 | return $this->relation('orderItemCompositeWithJoin'); 104 | } 105 | 106 | public function getOrderItemCompositeWithJoinQuery(): ActiveQuery 107 | { 108 | /** relations used by testFindCompositeWithJoin() */ 109 | return $this->hasOne(self::class, ['item_id' => 'item_id', 'order_id' => 'order_id' ])->joinWith('item'); 110 | } 111 | 112 | public function getOrderItemCompositeNoJoin(): self|null 113 | { 114 | return $this->relation('orderItemCompositeNoJoin'); 115 | } 116 | 117 | public function getOrderItemCompositeNoJoinQuery(): ActiveQuery 118 | { 119 | return $this->hasOne(self::class, ['item_id' => 'item_id', 'order_id' => 'order_id' ]); 120 | } 121 | 122 | public function getCustom(): Order|null 123 | { 124 | return $this->relation('custom'); 125 | } 126 | 127 | public function getCustomQuery(): ActiveQuery 128 | { 129 | return new ActiveQuery(Order::class); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/OrderItemWithNullFK.php: -------------------------------------------------------------------------------- 1 | $this->hasOne(CustomerWithFactory::class, ['id' => 'customer_id']), 18 | 'customerWithFactoryClosure' => $this->hasOne( 19 | fn () => $this->factory->create(CustomerWithFactory::class), 20 | ['id' => 'customer_id'] 21 | ), 22 | 'customerWithFactoryInstance' => $this->hasOne( 23 | $this->factory->create(CustomerWithFactory::class), 24 | ['id' => 'customer_id'] 25 | ), 26 | default => parent::relationQuery($name), 27 | }; 28 | } 29 | 30 | public function getCustomerWithFactory(): CustomerWithFactory|null 31 | { 32 | return $this->relation('customerWithFactory'); 33 | } 34 | 35 | public function getCustomerWithFactoryClosure(): CustomerWithFactory|null 36 | { 37 | return $this->relation('customerWithFactoryClosure'); 38 | } 39 | 40 | public function getCustomerWithFactoryInstance(): CustomerWithFactory|null 41 | { 42 | return $this->relation('customerWithFactoryInstance'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/OrderWithNullFK.php: -------------------------------------------------------------------------------- 1 | id; 27 | } 28 | 29 | public function getCustomerId(): int|null 30 | { 31 | return $this->customer_id; 32 | } 33 | 34 | public function getCreatedAt(): int 35 | { 36 | return $this->created_at; 37 | } 38 | 39 | public function getTotal(): float 40 | { 41 | return $this->total; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/Profile.php: -------------------------------------------------------------------------------- 1 | $this->hasMany(Item::class, ['id' => 'json_item_ids']) 28 | ->inverseOf('promotionsViaJson'), 29 | default => parent::relationQuery($name), 30 | }; 31 | } 32 | 33 | /** @return Item[] */ 34 | public function getItemsViaJson(): array 35 | { 36 | return $this->relation('itemsViaJson'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Stubs/ActiveRecord/TestTrigger.php: -------------------------------------------------------------------------------- 1 | hasMany(Beta::class, ['alpha_string_identifier' => 'string_identifier']); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Animal.php: -------------------------------------------------------------------------------- 1 | type = static::class; 27 | } 28 | 29 | public function getDoes() 30 | { 31 | return $this->does; 32 | } 33 | 34 | public function setDoes(string $value): void 35 | { 36 | $this->does = $value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/ArrayAndJsonTypes.php: -------------------------------------------------------------------------------- 1 | hasOne(Alpha::class, ['string_identifier' => 'alpha_string_identifier']); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/BitValues.php: -------------------------------------------------------------------------------- 1 | setDoes('meow'); 16 | } 17 | 18 | public function getException(): void 19 | { 20 | throw new Exception('no'); 21 | } 22 | 23 | /** 24 | * This is to test if __isset catches the error. 25 | * 26 | * @throw DivisionByZeroError 27 | */ 28 | public function getThrowable(): float|int 29 | { 30 | return 5 / 0; 31 | } 32 | 33 | public function setNonExistingProperty(string $value): void 34 | { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Category.php: -------------------------------------------------------------------------------- 1 | hasMany(Item::class, ['category_id' => 'id'])->onCondition(['item.id' => [1, 2, 3]]); 26 | } 27 | 28 | public function getItemsQuery(): ActiveQuery 29 | { 30 | return $this->hasMany(Item::class, ['category_id' => 'id']); 31 | } 32 | 33 | public function getOrderItemsQuery(): ActiveQuery 34 | { 35 | return $this->hasMany(OrderItem::class, ['item_id' => 'id'])->via('items'); 36 | } 37 | 38 | public function getOrdersQuery(): ActiveQuery 39 | { 40 | return $this->hasMany(Order::class, ['id' => 'order_id'])->via('orderItems'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Customer.php: -------------------------------------------------------------------------------- 1 | hasOne(Profile::class, ['id' => 'profile_id']); 45 | } 46 | 47 | public function getOrdersPlainQuery(): ActiveQuery 48 | { 49 | return $this->hasMany(Order::class, ['customer_id' => 'id']); 50 | } 51 | 52 | public function getOrdersQuery(): ActiveQuery 53 | { 54 | return $this->hasMany(Order::class, ['customer_id' => 'id'])->orderBy('[[id]]'); 55 | } 56 | 57 | public function getOrdersNoOrderQuery(): ActiveQuery 58 | { 59 | return $this->hasMany(Order::class, ['customer_id' => 'id']); 60 | } 61 | 62 | public function getExpensiveOrdersQuery(): ActiveQuery 63 | { 64 | return $this->hasMany(Order::class, ['customer_id' => 'id'])->andWhere('[[total]] > 50')->orderBy('id'); 65 | } 66 | 67 | public function getItemQuery(): void 68 | { 69 | } 70 | 71 | public function getOrdersWithItemsQuery(): ActiveQuery 72 | { 73 | return $this->hasMany(Order::class, ['customer_id' => 'id'])->with('orderItems'); 74 | } 75 | 76 | public function getExpensiveOrdersWithNullFKQuery(): ActiveQuery 77 | { 78 | return $this->hasMany( 79 | OrderWithNullFK::class, 80 | ['customer_id' => 'id'] 81 | )->andWhere('[[total]] > 50')->orderBy('id'); 82 | } 83 | 84 | public function getOrdersWithNullFKQuery(): ActiveQuery 85 | { 86 | return $this->hasMany(OrderWithNullFK::class, ['customer_id' => 'id'])->orderBy('id'); 87 | } 88 | 89 | public function getOrders2Query(): ActiveQuery 90 | { 91 | return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer2')->orderBy('id'); 92 | } 93 | 94 | /** deeply nested table relation */ 95 | public function getOrderItemsQuery(): ActiveQuery 96 | { 97 | $rel = $this->hasMany(Item::class, ['id' => 'item_id']); 98 | 99 | return $rel->viaTable('order_item', ['order_id' => 'id'], function ($q) { 100 | /* @var $q ActiveQuery */ 101 | $q->viaTable('order', ['customer_id' => 'id']); 102 | })->orderBy('id'); 103 | } 104 | 105 | public function setOrdersReadOnly(): void 106 | { 107 | } 108 | 109 | public function getOrderItems2Query(): ActiveQuery 110 | { 111 | return $this->hasMany(OrderItem::class, ['order_id' => 'id']) 112 | ->via('ordersNoOrder'); 113 | } 114 | 115 | public function getItems2Query(): ActiveQuery 116 | { 117 | return $this->hasMany(Item::class, ['id' => 'item_id']) 118 | ->via('orderItems2'); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/CustomerQuery.php: -------------------------------------------------------------------------------- 1 | andWhere('[[status]]=1'); 16 | 17 | return $this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/CustomerWithAlias.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | public function getEmail(): string 31 | { 32 | return $this->email; 33 | } 34 | 35 | public function getName(): string|null 36 | { 37 | return $this->name; 38 | } 39 | 40 | public function getAddress(): string|null 41 | { 42 | return $this->address; 43 | } 44 | 45 | public function getStatus(): int|null 46 | { 47 | return $this->get('status'); 48 | } 49 | 50 | public function getProfileQuery(): ActiveQuery 51 | { 52 | return $this->hasOne(Profile::class, ['id' => 'profile_id']); 53 | } 54 | 55 | public function getOrdersQuery(): ActiveQuery 56 | { 57 | return $this->hasMany(Order::class, ['customer_id' => 'id'])->orderBy('[[id]]'); 58 | } 59 | 60 | public function setEmail(string $email): void 61 | { 62 | $this->email = $email; 63 | } 64 | 65 | public function setName(string|null $name): void 66 | { 67 | $this->name = $name; 68 | } 69 | 70 | public function setAddress(string|null $address): void 71 | { 72 | $this->address = $address; 73 | } 74 | 75 | public function setStatus(int|null $status): void 76 | { 77 | $this->set('status', $status); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/DefaultPk.php: -------------------------------------------------------------------------------- 1 | hasMany( 27 | Employee::class, 28 | [ 29 | 'department_id' => 'id', 30 | ] 31 | )->inverseOf('department'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Document.php: -------------------------------------------------------------------------------- 1 | setDoes('bark'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Dossier.php: -------------------------------------------------------------------------------- 1 | hasOne( 29 | Employee::class, 30 | [ 31 | 'department_id' => 'department_id', 32 | 'id' => 'employee_id', 33 | ] 34 | )->inverseOf('dossier'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Employee.php: -------------------------------------------------------------------------------- 1 | first_name . ' ' . $this->last_name; 31 | } 32 | 33 | public function getDepartmentQuery(): ActiveQuery 34 | { 35 | return $this 36 | ->hasOne(Department::class, [ 37 | 'id' => 'department_id', 38 | ]) 39 | ->inverseOf('employees') 40 | ; 41 | } 42 | 43 | public function getDossierQuery(): ActiveQuery 44 | { 45 | return $this->hasOne( 46 | Dossier::class, 47 | [ 48 | 'department_id' => 'department_id', 49 | 'employee_id' => 'id', 50 | ] 51 | )->inverseOf('employee'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/Item.php: -------------------------------------------------------------------------------- 1 | hasOne(Category::class, ['id' => 'category_id']); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/NoExist.php: -------------------------------------------------------------------------------- 1 | get('created_at')); 34 | } 35 | 36 | public function setCreated_at(DateTimeInterface|int $createdAt): void 37 | { 38 | $this->set('created_at', $createdAt instanceof DateTimeInterface 39 | ? $createdAt->getTimestamp() 40 | : $createdAt); 41 | } 42 | 43 | public function setVirtualCustomerId(string|int|null $virtualCustomerId = null): void 44 | { 45 | $this->virtualCustomerId = $virtualCustomerId; 46 | } 47 | 48 | public function getVirtualCustomerQuery() 49 | { 50 | return $this->hasOne(Customer::class, ['id' => 'virtualCustomerId']); 51 | } 52 | 53 | public function getCustomerQuery(): ActiveQuery 54 | { 55 | return $this->hasOne(Customer::class, ['id' => 'customer_id']); 56 | } 57 | 58 | public function getCustomerJoinedWithProfileQuery(): ActiveQuery 59 | { 60 | return $this->hasOne(Customer::class, ['id' => 'customer_id'])->joinWith('profile'); 61 | } 62 | 63 | public function getCustomerJoinedWithProfileIndexOrderedQuery(): ActiveQuery 64 | { 65 | return $this->hasMany( 66 | Customer::class, 67 | ['id' => 'customer_id'] 68 | )->joinWith('profile')->orderBy(['profile.description' => SORT_ASC])->indexBy('name'); 69 | } 70 | 71 | public function getCustomer2Query(): ActiveQuery 72 | { 73 | return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders2'); 74 | } 75 | 76 | public function getOrderItemsQuery(): ActiveQuery 77 | { 78 | return $this->hasMany(OrderItem::class, ['order_id' => 'id']); 79 | } 80 | 81 | public function getOrderItems2Query(): ActiveQuery 82 | { 83 | return $this->hasMany(OrderItem::class, ['order_id' => 'id'])->indexBy('item_id'); 84 | } 85 | 86 | public function getOrderItems3Query(): ActiveQuery 87 | { 88 | return $this->hasMany( 89 | OrderItem::class, 90 | ['order_id' => 'id'] 91 | )->indexBy(fn ($row) => $row['order_id'] . '_' . $row['item_id']); 92 | } 93 | 94 | public function getOrderItemsWithNullFKQuery(): ActiveQuery 95 | { 96 | return $this->hasMany(OrderItemWithNullFK::class, ['order_id' => 'id']); 97 | } 98 | 99 | public function getItemsQuery(): ActiveQuery 100 | { 101 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems', static function ($q) { 102 | // additional query configuration 103 | })->orderBy('item.id'); 104 | } 105 | 106 | public function getItemsIndexedQuery(): ActiveQuery 107 | { 108 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems')->indexBy('id'); 109 | } 110 | 111 | public function getItemsWithNullFKQuery(): ActiveQuery 112 | { 113 | return $this->hasMany( 114 | Item::class, 115 | ['id' => 'item_id'] 116 | )->viaTable('order_item_with_null_fk', ['order_id' => 'id']); 117 | } 118 | 119 | public function getItemsInOrder1Query(): ActiveQuery 120 | { 121 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems', static function ($q) { 122 | $q->orderBy(['subtotal' => SORT_ASC]); 123 | })->orderBy('name'); 124 | } 125 | 126 | public function getItemsInOrder2Query(): ActiveQuery 127 | { 128 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems', static function ($q) { 129 | $q->orderBy(['subtotal' => SORT_DESC]); 130 | })->orderBy('name'); 131 | } 132 | 133 | public function getBooksQuery(): ActiveQuery 134 | { 135 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems')->where(['category_id' => 1]); 136 | } 137 | 138 | public function getBooksWithNullFKQuery(): ActiveQuery 139 | { 140 | return $this->hasMany( 141 | Item::class, 142 | ['id' => 'item_id'] 143 | )->via('orderItemsWithNullFK')->where(['category_id' => 1]); 144 | } 145 | 146 | public function getBooksViaTableQuery(): ActiveQuery 147 | { 148 | return $this->hasMany( 149 | Item::class, 150 | ['id' => 'item_id'] 151 | )->viaTable('order_item', ['order_id' => 'id'])->where(['category_id' => 1]); 152 | } 153 | 154 | public function getBooksWithNullFKViaTableQuery(): ActiveQuery 155 | { 156 | return $this->hasMany( 157 | Item::class, 158 | ['id' => 'item_id'] 159 | )->viaTable('order_item_with_null_fk', ['order_id' => 'id'])->where(['category_id' => 1]); 160 | } 161 | 162 | public function getBooks2Query(): ActiveQuery 163 | { 164 | return $this->hasMany( 165 | Item::class, 166 | ['id' => 'item_id'] 167 | )->onCondition(['category_id' => 1])->viaTable('order_item', ['order_id' => 'id']); 168 | } 169 | 170 | public function getBooksExplicitQuery(): ActiveQuery 171 | { 172 | return $this->hasMany( 173 | Item::class, 174 | ['id' => 'item_id'] 175 | )->onCondition(['category_id' => 1])->viaTable('order_item', ['order_id' => 'id']); 176 | } 177 | 178 | public function getBooksExplicitAQuery(): ActiveQuery 179 | { 180 | return $this->hasMany( 181 | Item::class, 182 | ['id' => 'item_id'] 183 | )->alias('bo')->onCondition(['bo.category_id' => 1])->viaTable('order_item', ['order_id' => 'id']); 184 | } 185 | 186 | public function getBookItemsQuery(): ActiveQuery 187 | { 188 | return $this->hasMany( 189 | Item::class, 190 | ['id' => 'item_id'] 191 | )->alias('books')->onCondition(['books.category_id' => 1])->viaTable('order_item', ['order_id' => 'id']); 192 | } 193 | 194 | public function getMovieItemsQuery(): ActiveQuery 195 | { 196 | return $this->hasMany( 197 | Item::class, 198 | ['id' => 'item_id'] 199 | )->alias('movies')->onCondition(['movies.category_id' => 2])->viaTable('order_item', ['order_id' => 'id']); 200 | } 201 | 202 | public function getLimitedItemsQuery(): ActiveQuery 203 | { 204 | return $this->hasMany(Item::class, ['id' => 'item_id'])->onCondition(['item.id' => [3, 5]])->via('orderItems'); 205 | } 206 | 207 | public function getExpensiveItemsUsingViaWithCallableQuery(): ActiveQuery 208 | { 209 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems', function (ActiveQuery $q) { 210 | $q->where(['>=', 'subtotal', 10]); 211 | }); 212 | } 213 | 214 | public function getCheapItemsUsingViaWithCallableQuery(): ActiveQuery 215 | { 216 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems', function (ActiveQuery $q) { 217 | $q->where(['<', 'subtotal', 10]); 218 | }); 219 | } 220 | 221 | public function getOrderItemsFor8Query(): ActiveQuery 222 | { 223 | return $this->hasMany(OrderItemWithNullFK::class, ['order_id' => 'id'])->andOnCondition(['subtotal' => 8.0]); 224 | } 225 | 226 | public function getItemsFor8Query(): ActiveQuery 227 | { 228 | return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItemsFor8'); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/OrderItem.php: -------------------------------------------------------------------------------- 1 | get('order_id'); 30 | $fields['item_id'] = $this->get('item_id'); 31 | $fields['price'] = $this->get('subtotal') / $this->get('quantity'); 32 | $fields['quantity'] = $this->get('quantity'); 33 | $fields['subtotal'] = $this->get('subtotal'); 34 | 35 | return $fields; 36 | } 37 | 38 | public function getOrderQuery(): ActiveQuery 39 | { 40 | return $this->hasOne(Order::class, ['id' => 'order_id']); 41 | } 42 | 43 | public function getItemQuery(): ActiveQuery 44 | { 45 | return $this->hasOne(Item::class, ['id' => 'item_id']); 46 | } 47 | 48 | public function getOrderItemCompositeWithJoinQuery(): ActiveQuery 49 | { 50 | /** relations used by testFindCompositeWithJoin() */ 51 | return $this->hasOne(self::class, ['item_id' => 'item_id', 'order_id' => 'order_id' ])->joinWith('item'); 52 | } 53 | 54 | public function getOrderItemCompositeNoJoinQuery(): ActiveQuery 55 | { 56 | return $this->hasOne(self::class, ['item_id' => 'item_id', 'order_id' => 'order_id' ]); 57 | } 58 | 59 | public function getCustomQuery(): ActiveQuery 60 | { 61 | return new ActiveQuery(Order::class); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Stubs/MagicActiveRecord/OrderItemWithNullFK.php: -------------------------------------------------------------------------------- 1 | getProperty($propertyName); 46 | 47 | $property->setAccessible(true); 48 | 49 | /** @psalm-var mixed $result */ 50 | $result = $property->getValue($object); 51 | } 52 | 53 | return $result; 54 | } 55 | 56 | /** 57 | * Invokes an inaccessible method. 58 | * 59 | * @param object $object The object to invoke the method on. 60 | * @param string $method The name of the method to invoke. 61 | * @param array $args The arguments to pass to the method. 62 | */ 63 | public static function invokeMethod(object $object, string $method, array $args = []): mixed 64 | { 65 | $reflection = new ReflectionObject($object); 66 | 67 | $result = null; 68 | 69 | if ($method !== '') { 70 | $method = $reflection->getMethod($method); 71 | 72 | $method->setAccessible(true); 73 | 74 | /** @psalm-var mixed $result */ 75 | $result = $method->invokeArgs($object, $args); 76 | } 77 | 78 | return $result; 79 | } 80 | 81 | /** 82 | * Sets an inaccessible object property to a designated value. 83 | */ 84 | public static function setInaccessibleProperty( 85 | object $object, 86 | string $propertyName, 87 | mixed $value 88 | ): void { 89 | $class = new ReflectionClass($object); 90 | 91 | if ($propertyName !== '') { 92 | $property = $class->getProperty($propertyName); 93 | $property->setValue($object, $value); 94 | } 95 | 96 | unset($class, $property); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Support/ConnectionHelper.php: -------------------------------------------------------------------------------- 1 | withDefinitions([ConnectionInterface::class => $db])); 24 | return new Factory($container, [ConnectionInterface::class => $db]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Support/DbHelper.php: -------------------------------------------------------------------------------- 1 | getDriverName(); 28 | 29 | $fixture = match ($driverName) { 30 | 'mysql' => dirname(__DIR__) . '/data/mysql.sql', 31 | 'oci' => dirname(__DIR__) . '/data/oci.sql', 32 | 'pgsql' => dirname(__DIR__) . '/data/pgsql.sql', 33 | 'sqlite' => dirname(__DIR__) . '/data/sqlite.sql', 34 | 'sqlsrv' => dirname(__DIR__) . '/data/mssql.sql', 35 | }; 36 | 37 | if ($db->isActive()) { 38 | $db->close(); 39 | } 40 | 41 | $db->open(); 42 | 43 | if ($driverName === 'oci') { 44 | [$drops, $creates] = explode('/* STATEMENTS */', file_get_contents($fixture), 2); 45 | [$statements, $triggers, $data] = explode('/* TRIGGERS */', $creates, 3); 46 | $lines = array_merge( 47 | explode('--', $drops), 48 | explode(';', $statements), 49 | explode('/', $triggers), 50 | explode(';', $data) 51 | ); 52 | } else { 53 | $lines = explode(';', file_get_contents($fixture)); 54 | } 55 | 56 | foreach ($lines as $line) { 57 | if (trim($line) !== '') { 58 | $db->getPDO()->exec($line); 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Adjust dbms specific escaping. 65 | * 66 | * @param string $sql string SQL statement to adjust. 67 | * @param string $driverName string DBMS name. 68 | * 69 | * @return mixed 70 | */ 71 | public static function replaceQuotes(string $sql, string $driverName): string 72 | { 73 | return match ($driverName) { 74 | 'mysql', 'sqlite' => str_replace(['[[', ']]'], '`', $sql), 75 | 'oci' => str_replace(['[[', ']]'], '"', $sql), 76 | 'pgsql' => str_replace(['\\[', '\\]'], ['[', ']'], preg_replace('/(\[\[)|((? str_replace(['[[', ']]'], ['[', ']'], $sql), 78 | default => $sql, 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Support/ModelFactory.php: -------------------------------------------------------------------------------- 1 | populateRecord($row); 18 | 19 | $models[] = $model; 20 | } 21 | 22 | return $models; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Support/MssqlHelper.php: -------------------------------------------------------------------------------- 1 | dsn, $this->username, $this->password); 21 | $pdoDriver->charset($this->charset); 22 | 23 | return new Connection($pdoDriver, $this->createSchemaCache()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Support/MysqlHelper.php: -------------------------------------------------------------------------------- 1 | dsn, $this->username, $this->password); 21 | $pdoDriver->charset($this->charset); 22 | 23 | return new Connection($pdoDriver, $this->createSchemaCache()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Support/OracleHelper.php: -------------------------------------------------------------------------------- 1 | dsn, $this->username, $this->password); 22 | $pdoDriver->charset($this->charset); 23 | $pdoDriver->attributes([PDO::ATTR_STRINGIFY_FETCHES => true]); 24 | 25 | return new Connection($pdoDriver, $this->createSchemaCache()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Support/PgsqlHelper.php: -------------------------------------------------------------------------------- 1 | dsn, $this->username, $this->password); 21 | $pdoDriver->charset($this->charset); 22 | 23 | return new Connection($pdoDriver, $this->createSchemaCache()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Support/SqliteHelper.php: -------------------------------------------------------------------------------- 1 | charset($this->charset); 19 | 20 | return new Connection($pdoDriver, $this->createSchemaCache()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | shouldReloadFixture = true; 23 | } 24 | 25 | protected static function reloadFixture(): void 26 | { 27 | ConnectionProvider::get()->close(); 28 | 29 | $db = static::createConnection(); 30 | ConnectionProvider::set($db); 31 | 32 | DbHelper::loadFixture($db); 33 | } 34 | 35 | protected static function db(): ConnectionInterface 36 | { 37 | return ConnectionProvider::get(); 38 | } 39 | 40 | public static function setUpBeforeClass(): void 41 | { 42 | parent::setUpBeforeClass(); 43 | 44 | $db = static::createConnection(); 45 | ConnectionProvider::set($db); 46 | DbHelper::loadFixture($db); 47 | } 48 | 49 | protected function tearDown(): void 50 | { 51 | if ($this->shouldReloadFixture) { 52 | $this->reloadFixture(); 53 | $this->shouldReloadFixture = false; 54 | } 55 | 56 | parent::tearDown(); 57 | } 58 | 59 | public static function tearDownAfterClass(): void 60 | { 61 | ConnectionProvider::get()->close(); 62 | ConnectionProvider::remove(); 63 | 64 | parent::tearDownAfterClass(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/data/runtime/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiisoft/active-record/4d39f1fc1eafff7c3b7adbbc77d137adc6ad5733/tests/data/runtime/.gitignore --------------------------------------------------------------------------------