├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Adapters │ └── D7Adapter.php ├── Debug │ ├── IlluminateQueryDebugger.php │ └── debug_info.php ├── Exceptions │ └── ExceptionFromBitrix.php ├── Helpers.php ├── Models │ ├── ArrayableModel.php │ ├── BaseBitrixModel.php │ ├── BitrixModel.php │ ├── D7Model.php │ ├── ElementModel.php │ ├── EloquentModel.php │ ├── SectionModel.php │ ├── Traits │ │ ├── DeactivationTrait.php │ │ ├── HidesAttributes.php │ │ └── ModelEventsTrait.php │ └── UserModel.php ├── Queries │ ├── BaseQuery.php │ ├── BaseRelationQuery.php │ ├── D7Query.php │ ├── ElementQuery.php │ ├── OldCoreQuery.php │ ├── SectionQuery.php │ └── UserQuery.php └── ServiceProvider.php └── tests ├── D7ModelTest.php ├── D7QueryTest.php ├── ElementModelTest.php ├── ElementQueryTest.php ├── RelationTest.php ├── SectionModelTest.php ├── SectionQueryTest.php ├── Stubs ├── BxUserWithAuth.php ├── BxUserWithoutAuth.php ├── TestD7Element.php ├── TestD7Element2.php ├── TestD7ElementClass.php ├── TestD7ElementClass2.php ├── TestD7ResultObject.php ├── TestElement.php ├── TestElement2.php ├── TestSection.php └── TestUser.php ├── TestCase.php ├── UserModelTest.php └── UserQueryTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | /.idea 6 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: 3 | - 'src/*' 4 | excluded_paths: 5 | - 'vendor/*' 6 | - 'tests/*' 7 | tools: 8 | php_cs_fixer: 9 | config: { level: psr2 } 10 | checks: 11 | php: 12 | code_rating: true 13 | duplication: true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | - 7.4 10 | - 8.0 11 | 12 | before_script: 13 | - composer self-update 14 | - composer install --prefer-source --no-interaction 15 | 16 | script: 17 | - vendor/bin/phpunit 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nekrasov Ilya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ge1i0n/bitrix-models", 3 | "license": "MIT", 4 | "keywords": [ 5 | "bitrix", 6 | "models" 7 | ], 8 | "authors": [ 9 | { 10 | "name": "Nekrasov Ilya", 11 | "email": "nekrasov.ilya90@gmail.com" 12 | } 13 | ], 14 | "homepage": "https://github.com/ge1i0n/bitrix-models", 15 | "require": { 16 | "php": "^8.2", 17 | "illuminate/support": "^11.0", 18 | "illuminate/pagination": "^11.0" 19 | }, 20 | "suggest": { 21 | "arrilot/bitrix-blade": "To render pagination views using ->links()", 22 | "illuminate/database": "5.*|6.*|7.*|8.*|9.*|10.*|11.* to use Eloquent models", 23 | "illuminate/events": "5.*|6.*|7.*|8.*|9.*|10.*|11.* to use events in Eloquent models" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^5.0", 27 | "mockery/mockery": "~1" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Arrilot\\BitrixModels\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Arrilot\\Tests\\BitrixModels\\": "tests/" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Adapters/D7Adapter.php: -------------------------------------------------------------------------------- 1 | className = $className; 31 | } 32 | 33 | /** 34 | * Getter for class name. 35 | * 36 | * @return string 37 | */ 38 | public function getClassName() 39 | { 40 | return $this->className; 41 | } 42 | 43 | /** 44 | * Handle dynamic method calls into a static calls on bitrix entity class. 45 | * 46 | * @param string $method 47 | * @param array $parameters 48 | * @return mixed 49 | */ 50 | public function __call($method, $parameters) 51 | { 52 | $className = $this->className; 53 | 54 | return $className::$method(...$parameters); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Debug/IlluminateQueryDebugger.php: -------------------------------------------------------------------------------- 1 | ShowSqlStat && ($USER->CanDoOperation('edit_php') || $_SESSION["SHOW_SQL_STAT"] == "Y")); 16 | if ($bShowStat && class_exists(Manager::class) && Manager::logging()) { 17 | require_once(__DIR__ . '/debug_info.php'); 18 | } 19 | } 20 | } 21 | 22 | public static function interpolateQuery($query, $params) 23 | { 24 | $keys = array(); 25 | 26 | # build a regular expression for each parameter 27 | foreach ($params as $key => $value) { 28 | $keys[] = is_string($key) ? '/:' . $key . '/' : '/[?]/'; 29 | $params[$key] = "'" . $value . "'"; 30 | } 31 | 32 | return preg_replace($keys, $params, $query, 1, $count); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Debug/debug_info.php: -------------------------------------------------------------------------------- 1 | '; 22 | echo 'Статистика SQL запросов illuminate/database
'; 23 | echo '' . 'Всего SQL запросов: ' . " " . intval($totalQueryCount) . "
"; 24 | echo "Время исполнения запросов: " . round($totalQueryTime, 4) . " сек.
"; 25 | echo '
'; 26 | 27 | //CJSPopup 28 | require_once($_SERVER["DOCUMENT_ROOT"] . BX_ROOT . "/modules/main/interface/admin_lib.php"); 29 | ?> 30 | 33 | jsPopup = 'BX_DEBUG_INFO_ILLUMINATE'; 36 | $obJSPopup->StartDescription('bx-core-debug-info'); 37 | ?> 38 |

Всего запросов: , время: сек.

39 |

Поиск:

40 | StartContent(['buffer' => true]); 42 | if (count($queryLog) > 0) { 43 | ?>
$arQueryDebug) { 46 | $strSql = $arQueryDebug["query"]; 47 | $arQueries[$strSql]["COUNT"]++; 48 | $arQueries[$strSql]["CALLS"][] = [ 49 | "QUERY" => $strSql, 50 | "BINDINGS" => $arQueryDebug['bindings'], 51 | "TIME" => $arQueryDebug["time"] / 1000, 52 | ]; 53 | } 54 | ?> $query) { 57 | ?> 58 | 59 | 60 | 67 |
 ()
71 |
#DIVIDER#
72 | $query) { 75 | ?> 103 | 107 |
108 | StartButtons(); 111 | $obJSPopup->ShowStandardButtons(array('close')); 112 | -------------------------------------------------------------------------------- /src/Exceptions/ExceptionFromBitrix.php: -------------------------------------------------------------------------------- 1 | $primaryModel) { 35 | if ($multiple && is_array($keys = $primaryModel[$primaryKey])) { 36 | $value = []; 37 | foreach ($keys as $key) { 38 | $key = static::normalizeModelKey($key); 39 | if (isset($buckets[$key])) { 40 | $value = array_merge($value, $buckets[$key]); 41 | } 42 | } 43 | } else { 44 | $key = static::normalizeModelKey($primaryModel[$primaryKey]); 45 | $value = isset($buckets[$key]) ? $buckets[$key] : ($multiple ? [] : null); 46 | } 47 | 48 | $primaryModel->populateRelation($relationName, is_array($value) ? (new Collection($value))->keyBy(function ($item) { 49 | return $item->id; 50 | }) : $value); 51 | } 52 | } 53 | 54 | /** 55 | * Сгруппировать найденные модели 56 | * @param array $models 57 | * @param string $linkKey 58 | * @param bool $multiple 59 | * @return array 60 | */ 61 | protected static function buildBuckets($models, $linkKey, $multiple) 62 | { 63 | $buckets = []; 64 | 65 | foreach ($models as $model) { 66 | $key = $model[$linkKey]; 67 | if (is_scalar($key)) { 68 | $buckets[$key][] = $model; 69 | } elseif (is_array($key)) { 70 | foreach ($key as $k) { 71 | $k = static::normalizeModelKey($k); 72 | $buckets[$k][] = $model; 73 | } 74 | } else { 75 | $key = static::normalizeModelKey($key); 76 | $buckets[$key][] = $model; 77 | } 78 | } 79 | 80 | if (!$multiple) { 81 | foreach ($buckets as $i => $bucket) { 82 | $buckets[$i] = reset($bucket); 83 | } 84 | } 85 | 86 | return $buckets; 87 | } 88 | 89 | /** 90 | * @param mixed $value raw key value. 91 | * @return string normalized key value. 92 | */ 93 | protected static function normalizeModelKey($value) 94 | { 95 | if (is_object($value) && method_exists($value, '__toString')) { 96 | // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId` 97 | $value = $value->__toString(); 98 | } 99 | 100 | return $value; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Models/ArrayableModel.php: -------------------------------------------------------------------------------- 1 | fields[] = $value; 71 | } else { 72 | $this->fields[$offset] = $value; 73 | } 74 | } 75 | 76 | /** 77 | * Exists method for ArrayIterator. 78 | * 79 | * @param $offset 80 | * 81 | * @return bool 82 | */ 83 | public function offsetExists($offset) 84 | { 85 | return $this->getAccessor($offset) || $this->getAccessorForLanguageField($offset) 86 | ? true : isset($this->fields[$offset]); 87 | } 88 | 89 | /** 90 | * Unset method for ArrayIterator. 91 | * 92 | * @param $offset 93 | * 94 | * @return void 95 | */ 96 | public function offsetUnset($offset) 97 | { 98 | unset($this->fields[$offset]); 99 | } 100 | 101 | /** 102 | * Get method for ArrayIterator. 103 | * 104 | * @param $offset 105 | * 106 | * @return mixed 107 | */ 108 | public function offsetGet($offset) 109 | { 110 | $fieldValue = isset($this->fields[$offset]) ? $this->fields[$offset] : null; 111 | $accessor = $this->getAccessor($offset); 112 | if ($accessor) { 113 | return $this->$accessor($fieldValue); 114 | } 115 | 116 | $accessorForLanguageField = $this->getAccessorForLanguageField($offset); 117 | if ($accessorForLanguageField) { 118 | return $this->$accessorForLanguageField($offset); 119 | } 120 | 121 | return $fieldValue; 122 | } 123 | 124 | /** 125 | * Get an iterator for fields. 126 | * 127 | * @return ArrayIterator 128 | */ 129 | public function getIterator() 130 | { 131 | return new ArrayIterator($this->fields); 132 | } 133 | 134 | /** 135 | * Get accessor method name if it exists. 136 | * 137 | * @param string $field 138 | * 139 | * @return string|false 140 | */ 141 | private function getAccessor($field) 142 | { 143 | $method = 'get' . Str::camel($field) . 'Attribute'; 144 | 145 | return method_exists($this, $method) ? $method : false; 146 | } 147 | 148 | /** 149 | * Get accessor for language field method name if it exists. 150 | * 151 | * @param string $field 152 | * 153 | * @return string|false 154 | */ 155 | private function getAccessorForLanguageField($field) 156 | { 157 | $method = 'getValueFromLanguageField'; 158 | 159 | return in_array($field, $this->languageAccessors) && method_exists($this, $method) ? $method : false; 160 | } 161 | 162 | /** 163 | * Add value to append. 164 | * 165 | * @param array|string $attributes 166 | * @return $this 167 | */ 168 | public function append($attributes) 169 | { 170 | $this->appends = array_unique( 171 | array_merge($this->appends, is_string($attributes) ? func_get_args() : $attributes) 172 | ); 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * Setter for appends. 179 | * 180 | * @param array $appends 181 | * @return $this 182 | */ 183 | public function setAppends(array $appends) 184 | { 185 | $this->appends = $appends; 186 | 187 | return $this; 188 | } 189 | 190 | /** 191 | * Cast model to array. 192 | * 193 | * @return array 194 | */ 195 | public function toArray() 196 | { 197 | $array = $this->fields; 198 | 199 | foreach ($this->appends as $accessor) { 200 | if (isset($this[$accessor])) { 201 | $array[$accessor] = $this[$accessor]; 202 | } 203 | } 204 | 205 | foreach ($this->related as $key => $value) { 206 | if (is_object($value) && method_exists($value, 'toArray')) { 207 | $array[$key] = $value->toArray(); 208 | } elseif (is_null($value) || $value === false) { 209 | $array[$key] = $value; 210 | } 211 | } 212 | 213 | if (count($this->getVisible()) > 0) { 214 | $array = array_intersect_key($array, array_flip($this->getVisible())); 215 | } 216 | 217 | if (count($this->getHidden()) > 0) { 218 | $array = array_diff_key($array, array_flip($this->getHidden())); 219 | } 220 | 221 | return $array; 222 | } 223 | 224 | /** 225 | * Convert model to json. 226 | * 227 | * @param int $options 228 | * 229 | * @return string 230 | */ 231 | public function toJson($options = 0) 232 | { 233 | return json_encode($this->toArray(), $options); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Models/BaseBitrixModel.php: -------------------------------------------------------------------------------- 1 | load(); 83 | 84 | return $this->fields; 85 | } 86 | 87 | /** 88 | * Load model fields from database if they are not loaded yet. 89 | * 90 | * @return $this 91 | */ 92 | public function load() 93 | { 94 | if (!$this->fieldsAreFetched) { 95 | $this->refresh(); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * Get model fields from cache or database. 103 | * 104 | * @return array 105 | */ 106 | public function getFields() 107 | { 108 | if ($this->fieldsAreFetched) { 109 | return $this->fields; 110 | } 111 | 112 | return $this->refreshFields(); 113 | } 114 | 115 | /** 116 | * Refresh model from database and place data to $this->fields. 117 | * 118 | * @return array 119 | */ 120 | public function refresh() 121 | { 122 | return $this->refreshFields(); 123 | } 124 | 125 | /** 126 | * Refresh model fields and save them to a class field. 127 | * 128 | * @return array 129 | */ 130 | public function refreshFields() 131 | { 132 | if ($this->id === null) { 133 | $this->original = []; 134 | return $this->fields = []; 135 | } 136 | 137 | $this->fields = static::query()->getById($this->id)->fields; 138 | $this->original = $this->fields; 139 | 140 | $this->fieldsAreFetched = true; 141 | 142 | return $this->fields; 143 | } 144 | 145 | /** 146 | * Fill model fields if they are already known. 147 | * Saves DB queries. 148 | * 149 | * @param array $fields 150 | * 151 | * @return void 152 | */ 153 | public function fill($fields) 154 | { 155 | if (!is_array($fields)) { 156 | return; 157 | } 158 | 159 | if (isset($fields['ID'])) { 160 | $this->id = $fields['ID']; 161 | } 162 | 163 | $this->fields = $fields; 164 | 165 | $this->fieldsAreFetched = true; 166 | 167 | if (method_exists($this, 'afterFill')) { 168 | $this->afterFill(); 169 | } 170 | 171 | $this->original = $this->fields; 172 | } 173 | 174 | /** 175 | * Set current model id. 176 | * 177 | * @param $id 178 | */ 179 | protected function setId($id) 180 | { 181 | $this->id = $id; 182 | $this->fields['ID'] = $id; 183 | } 184 | 185 | /** 186 | * Create new item in database. 187 | * 188 | * @param $fields 189 | * 190 | * @throws LogicException 191 | * 192 | * @return static|bool 193 | */ 194 | public static function create($fields) 195 | { 196 | return static::internalCreate($fields); 197 | } 198 | 199 | /** 200 | * Get count of items that match $filter. 201 | * 202 | * @param array $filter 203 | * 204 | * @return int 205 | */ 206 | public static function count(array $filter = []) 207 | { 208 | return static::query()->filter($filter)->count(); 209 | } 210 | 211 | /** 212 | * Get item by its id. 213 | * 214 | * @param int $id 215 | * 216 | * @return static|bool 217 | */ 218 | public static function find($id) 219 | { 220 | return static::query()->getById($id); 221 | } 222 | 223 | /** 224 | * Update model. 225 | * 226 | * @param array $fields 227 | * 228 | * @return bool 229 | */ 230 | public function update(array $fields = []) 231 | { 232 | $keys = []; 233 | foreach ($fields as $key => $value) { 234 | Arr::set($this->fields, $key, $value); 235 | $keys[] = $key; 236 | } 237 | 238 | return $this->save($keys); 239 | } 240 | 241 | /** 242 | * Create an array of fields that will be saved to database. 243 | * 244 | * @param $selectedFields 245 | * 246 | * @return array|null 247 | */ 248 | protected function normalizeFieldsForSave($selectedFields) 249 | { 250 | $fields = []; 251 | if ($this->fields === null) { 252 | return []; 253 | } 254 | 255 | foreach ($this->fields as $field => $value) { 256 | if (!$this->fieldShouldNotBeSaved($field, $value, $selectedFields)) { 257 | $fields[$field] = $value; 258 | } 259 | } 260 | 261 | return $fields ?: null; 262 | } 263 | 264 | /** 265 | * Instantiate a query object for the model. 266 | * 267 | * @throws LogicException 268 | * 269 | * @return BaseQuery 270 | */ 271 | public static function query() 272 | { 273 | throw new LogicException('public static function query() is not implemented'); 274 | } 275 | 276 | /** 277 | * Handle dynamic static method calls into a new query. 278 | * 279 | * @param string $method 280 | * @param array $parameters 281 | * @return mixed 282 | */ 283 | public static function __callStatic($method, $parameters) 284 | { 285 | return static::query()->$method(...$parameters); 286 | } 287 | 288 | /** 289 | * Returns the value of a model property. 290 | * 291 | * This method will check in the following order and act accordingly: 292 | * 293 | * - a property defined by a getter: return the getter result 294 | * 295 | * Do not call this method directly as it is a PHP magic method that 296 | * will be implicitly called when executing `$value = $component->property;`. 297 | * @param string $name the property name 298 | * @return mixed the property value 299 | * @throws \Exception if the property is not defined 300 | * @see __set() 301 | */ 302 | public function __get($name) 303 | { 304 | // Если уже сохранен такой релейшн, то возьмем его 305 | if (isset($this->related[$name]) || array_key_exists($name, $this->related)) { 306 | return $this->related[$name]; 307 | } 308 | 309 | // Если нет сохраненных данных, ищем подходящий геттер 310 | $getter = $name; 311 | if (method_exists($this, $getter)) { 312 | // read property, e.g. getName() 313 | $value = $this->$getter(); 314 | 315 | // Если геттер вернул запрос, значит $name - релейшен. Нужно выполнить запрос и сохранить во внутренний массив 316 | if ($value instanceof BaseQuery) { 317 | $this->related[$name] = $value->findFor(); 318 | return $this->related[$name]; 319 | } 320 | } 321 | 322 | throw new \Exception('Getting unknown property: ' . get_class($this) . '::' . $name); 323 | } 324 | 325 | /** 326 | * Получить запрос для релейшена по имени 327 | * @param string $name - название релейшена, например `orders` для релейшена, определенного через метод getOrders() 328 | * @param bool $throwException - кидать ли исключение в случае ошибки 329 | * @return BaseQuery - запрос для подгрузки релейшена 330 | * @throws \InvalidArgumentException 331 | */ 332 | public function getRelation($name, $throwException = true) 333 | { 334 | $getter = $name; 335 | try { 336 | $relation = $this->$getter(); 337 | } catch (\BadMethodCallException $e) { 338 | if ($throwException) { 339 | throw new \InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); 340 | } 341 | 342 | return null; 343 | } 344 | 345 | if (!$relation instanceof BaseQuery) { 346 | if ($throwException) { 347 | throw new \InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".'); 348 | } 349 | 350 | return null; 351 | } 352 | 353 | return $relation; 354 | } 355 | 356 | /** 357 | * Reset event errors back to default. 358 | */ 359 | protected function resetEventErrors() 360 | { 361 | $this->eventErrors = []; 362 | } 363 | 364 | /** 365 | * Declares a `has-one` relation. 366 | * The declaration is returned in terms of a relational [[BaseQuery]] instance 367 | * through which the related record can be queried and retrieved back. 368 | * 369 | * A `has-one` relation means that there is at most one related record matching 370 | * the criteria set by this relation, e.g., a customer has one country. 371 | * 372 | * For example, to declare the `country` relation for `Customer` class, we can write 373 | * the following code in the `Customer` class: 374 | * 375 | * ```php 376 | * public function country() 377 | * { 378 | * return $this->hasOne(Country::className(), 'ID', 'PROPERTY_COUNTRY'); 379 | * } 380 | * ``` 381 | * 382 | * Note that in the above, the 'ID' key in the `$link` parameter refers to an attribute name 383 | * in the related class `Country`, while the 'PROPERTY_COUNTRY' value refers to an attribute name 384 | * in the current BaseBitrixModel class. 385 | * 386 | * Call methods declared in [[BaseQuery]] to further customize the relation. 387 | * 388 | * @param string $class the class name of the related record 389 | * @param string $foreignKey 390 | * @param string $localKey 391 | * @return BaseQuery the relational query object. 392 | */ 393 | public function hasOne($class, $foreignKey, $localKey = 'ID') 394 | { 395 | return $this->createRelationQuery($class, $foreignKey, $localKey, false); 396 | } 397 | 398 | /** 399 | * Declares a `has-many` relation. 400 | * The declaration is returned in terms of a relational [[BaseQuery]] instance 401 | * through which the related record can be queried and retrieved back. 402 | * 403 | * A `has-many` relation means that there are multiple related records matching 404 | * the criteria set by this relation, e.g., a customer has many orders. 405 | * 406 | * For example, to declare the `orders` relation for `Customer` class, we can write 407 | * the following code in the `Customer` class: 408 | * 409 | * ```php 410 | * public function orders() 411 | * { 412 | * return $this->hasMany(Order::className(), 'PROPERTY_COUNTRY_VALUE', 'ID'); 413 | * } 414 | * ``` 415 | * 416 | * Note that in the above, the 'customer_id' key in the `$link` parameter refers to 417 | * an attribute name in the related class `Order`, while the 'id' value refers to 418 | * an attribute name in the current BaseBitrixModel class. 419 | * 420 | * Call methods declared in [[BaseQuery]] to further customize the relation. 421 | * 422 | * @param string $class the class name of the related record 423 | * @param string $foreignKey 424 | * @param string $localKey 425 | * @return BaseQuery the relational query object. 426 | */ 427 | public function hasMany($class, $foreignKey, $localKey = 'ID') 428 | { 429 | return $this->createRelationQuery($class, $foreignKey, $localKey, true); 430 | } 431 | 432 | /** 433 | * Creates a query instance for `has-one` or `has-many` relation. 434 | * @param string $class the class name of the related record. 435 | * @param string $foreignKey 436 | * @param string $localKey 437 | * @param bool $multiple whether this query represents a relation to more than one record. 438 | * @return BaseQuery the relational query object. 439 | * @see hasOne() 440 | * @see hasMany() 441 | */ 442 | protected function createRelationQuery($class, $foreignKey, $localKey, $multiple) 443 | { 444 | /* @var $class BaseBitrixModel */ 445 | /* @var $query BaseQuery */ 446 | $query = $class::query(); 447 | $query->foreignKey = $localKey; 448 | $query->localKey = $foreignKey; 449 | $query->primaryModel = $this; 450 | $query->multiple = $multiple; 451 | return $query; 452 | } 453 | 454 | /** 455 | * Записать модели как связанные 456 | * @param string $name - название релейшена 457 | * @param Collection|BaseBitrixModel $records - связанные модели 458 | * @see getRelation() 459 | */ 460 | public function populateRelation($name, $records) 461 | { 462 | $this->related[$name] = $records; 463 | } 464 | 465 | /** 466 | * Setter for currentLanguage. 467 | * 468 | * @param $language 469 | * @return mixed 470 | */ 471 | public static function setCurrentLanguage($language) 472 | { 473 | self::$currentLanguage = $language; 474 | } 475 | 476 | /** 477 | * Getter for currentLanguage. 478 | * 479 | * @return string 480 | */ 481 | public static function getCurrentLanguage() 482 | { 483 | return self::$currentLanguage; 484 | } 485 | 486 | /** 487 | * Get value from language field according to current language. 488 | * 489 | * @param $field 490 | * @return mixed 491 | */ 492 | protected function getValueFromLanguageField($field) 493 | { 494 | $key = $field . '_' . $this->getCurrentLanguage(); 495 | 496 | return isset($this->fields[$key]) ? $this->fields[$key] : null; 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /src/Models/BitrixModel.php: -------------------------------------------------------------------------------- 1 | 'Fetch', 32 | 'params' => [], 33 | ]; 34 | 35 | /** 36 | * Constructor. 37 | * 38 | * @param $id 39 | * @param $fields 40 | */ 41 | public function __construct($id = null, $fields = null) 42 | { 43 | static::instantiateObject(); 44 | 45 | $this->id = $id; 46 | 47 | $this->fill($fields); 48 | } 49 | 50 | /** 51 | * Activate model. 52 | * 53 | * @return bool 54 | */ 55 | public function activate() 56 | { 57 | $this->fields['ACTIVE'] = 'Y'; 58 | 59 | return $this->save(['ACTIVE']); 60 | } 61 | 62 | /** 63 | * Deactivate model. 64 | * 65 | * @return bool 66 | */ 67 | public function deactivate() 68 | { 69 | $this->fields['ACTIVE'] = 'N'; 70 | 71 | return $this->save(['ACTIVE']); 72 | } 73 | 74 | /** 75 | * Internal part of create to avoid problems with static and inheritance 76 | * 77 | * @param $fields 78 | * 79 | * @throws ExceptionFromBitrix 80 | * 81 | * @return static|bool 82 | */ 83 | protected static function internalCreate($fields) 84 | { 85 | $model = new static(null, $fields); 86 | 87 | if ($model->onBeforeSave() === false || $model->onBeforeCreate() === false) { 88 | return false; 89 | } 90 | 91 | $bxObject = static::instantiateObject(); 92 | $id = static::internalDirectCreate($bxObject, $model->fields); 93 | $model->setId($id); 94 | 95 | $result = $id ? true : false; 96 | 97 | $model->setEventErrorsOnFail($result, $bxObject); 98 | $model->onAfterCreate($result); 99 | $model->onAfterSave($result); 100 | $model->resetEventErrors(); 101 | $model->throwExceptionOnFail($result, $bxObject); 102 | 103 | return $model; 104 | } 105 | 106 | public static function internalDirectCreate($bxObject, $fields) 107 | { 108 | return $bxObject->add($fields); 109 | } 110 | 111 | /** 112 | * Delete model. 113 | * 114 | * @return bool 115 | * @throws ExceptionFromBitrix 116 | */ 117 | public function delete() 118 | { 119 | if ($this->onBeforeDelete() === false) { 120 | return false; 121 | } 122 | 123 | $result = static::$bxObject->delete($this->id); 124 | 125 | $this->setEventErrorsOnFail($result, static::$bxObject); 126 | $this->onAfterDelete($result); 127 | $this->resetEventErrors(); 128 | $this->throwExceptionOnFail($result, static::$bxObject); 129 | 130 | return $result; 131 | } 132 | 133 | /** 134 | * Save model to database. 135 | * 136 | * @param array $selectedFields save only these fields instead of all. 137 | * @return bool 138 | * @throws ExceptionFromBitrix 139 | */ 140 | public function save($selectedFields = []) 141 | { 142 | $fieldsSelectedForSave = is_array($selectedFields) ? $selectedFields : func_get_args(); 143 | $this->fieldsSelectedForSave = $fieldsSelectedForSave; 144 | if ($this->onBeforeSave() === false || $this->onBeforeUpdate() === false) { 145 | $this->fieldsSelectedForSave = []; 146 | return false; 147 | } else { 148 | $this->fieldsSelectedForSave = []; 149 | } 150 | 151 | $fields = $this->normalizeFieldsForSave($fieldsSelectedForSave); 152 | $result = $fields === null 153 | ? true 154 | : $this->internalUpdate($fields, $fieldsSelectedForSave); 155 | 156 | $this->setEventErrorsOnFail($result, static::$bxObject); 157 | $this->onAfterUpdate($result); 158 | $this->onAfterSave($result); 159 | $this->resetEventErrors(); 160 | $this->throwExceptionOnFail($result, static::$bxObject); 161 | 162 | return $result; 163 | } 164 | 165 | /** 166 | * @param $fields 167 | * @param $fieldsSelectedForSave 168 | * @return bool 169 | */ 170 | protected function internalUpdate($fields, $fieldsSelectedForSave) 171 | { 172 | return !empty($fields) ? static::$bxObject->update($this->id, $fields) : false; 173 | } 174 | 175 | /** 176 | * Scope to get only active items. 177 | * 178 | * @param BaseQuery $query 179 | * 180 | * @return BaseQuery 181 | */ 182 | public function scopeActive($query) 183 | { 184 | $query->filter['ACTIVE'] = 'Y'; 185 | 186 | return $query; 187 | } 188 | 189 | /** 190 | * Determine whether the field should be stopped from passing to "update". 191 | * 192 | * @param string $field 193 | * @param mixed $value 194 | * @param array $selectedFields 195 | * 196 | * @return bool 197 | */ 198 | protected function fieldShouldNotBeSaved($field, $value, $selectedFields) 199 | { 200 | $blacklistedFields = [ 201 | 'ID', 202 | 'IBLOCK_ID', 203 | 'GROUPS', 204 | ]; 205 | 206 | return (!empty($selectedFields) && !in_array($field, $selectedFields)) 207 | || in_array($field, $blacklistedFields) 208 | || ($field[0] === '~') 209 | || (substr($field, 0, 9) === 'PROPERTY_') 210 | || (is_array($this->original) && array_key_exists($field, $this->original) && $this->original[$field] === $value); 211 | } 212 | 213 | /** 214 | * Instantiate bitrix entity object. 215 | * 216 | * @throws LogicException 217 | * 218 | * @return object 219 | */ 220 | public static function instantiateObject() 221 | { 222 | if (static::$bxObject) { 223 | return static::$bxObject; 224 | } 225 | 226 | if (class_exists(static::$objectClass)) { 227 | return static::$bxObject = new static::$objectClass(); 228 | } 229 | 230 | throw new LogicException('Object initialization failed'); 231 | } 232 | 233 | /** 234 | * Destroy bitrix entity object. 235 | * 236 | * @return void 237 | */ 238 | public static function destroyObject() 239 | { 240 | static::$bxObject = null; 241 | } 242 | 243 | /** 244 | * Set eventErrors field on error. 245 | * 246 | * @param bool $result 247 | * @param object $bxObject 248 | */ 249 | protected function setEventErrorsOnFail($result, $bxObject) 250 | { 251 | if (!$result) { 252 | $this->eventErrors = (array) $bxObject->LAST_ERROR; 253 | } 254 | } 255 | 256 | /** 257 | * Throw bitrix exception on fail 258 | * 259 | * @param bool $result 260 | * @param object $bxObject 261 | * @throws ExceptionFromBitrix 262 | */ 263 | protected function throwExceptionOnFail($result, $bxObject) 264 | { 265 | if (!$result) { 266 | throw new ExceptionFromBitrix($bxObject->LAST_ERROR); 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Models/D7Model.php: -------------------------------------------------------------------------------- 1 | id = $id; 68 | $this->fill($fields); 69 | static::instantiateAdapter(); 70 | } 71 | 72 | /** 73 | * Setter for adapter (for testing) 74 | * @param $adapter 75 | */ 76 | public static function setAdapter($adapter) 77 | { 78 | static::$adapters[get_called_class()] = $adapter; 79 | } 80 | 81 | /** 82 | * Instantiate adapter if it's not instantiated. 83 | * 84 | * @return D7Adapter 85 | */ 86 | public static function instantiateAdapter() 87 | { 88 | $class = get_called_class(); 89 | if (isset(static::$adapters[$class])) { 90 | return static::$adapters[$class]; 91 | } 92 | 93 | return static::$adapters[$class] = new D7Adapter(static::cachedTableClass()); 94 | } 95 | 96 | /** 97 | * Instantiate a query object for the model. 98 | * 99 | * @return D7Query 100 | */ 101 | public static function query() 102 | { 103 | return new D7Query(static::instantiateAdapter(), get_called_class()); 104 | } 105 | 106 | /** 107 | * @return string 108 | * @throws LogicException 109 | */ 110 | public static function tableClass() 111 | { 112 | $tableClass = static::TABLE_CLASS; 113 | if (!$tableClass) { 114 | throw new LogicException('You must set TABLE_CLASS constant inside a model or override tableClass() method'); 115 | } 116 | 117 | return $tableClass; 118 | } 119 | 120 | /** 121 | * Cached version of table class. 122 | * 123 | * @return string 124 | */ 125 | public static function cachedTableClass() 126 | { 127 | $class = get_called_class(); 128 | if (!isset(static::$cachedTableClasses[$class])) { 129 | static::$cachedTableClasses[$class] = static::tableClass(); 130 | } 131 | 132 | return static::$cachedTableClasses[$class]; 133 | } 134 | 135 | /** 136 | * Internal part of create to avoid problems with static and inheritance 137 | * 138 | * @param $fields 139 | * 140 | * @throws ExceptionFromBitrix 141 | * 142 | * @return static|bool 143 | */ 144 | protected static function internalCreate($fields) 145 | { 146 | $model = new static(null, $fields); 147 | 148 | if ($model->onBeforeSave() === false || $model->onBeforeCreate() === false) { 149 | return false; 150 | } 151 | 152 | $resultObject = static::instantiateAdapter()->add($model->fields); 153 | $result = $resultObject->isSuccess(); 154 | if ($result) { 155 | $model->setId($resultObject->getId()); 156 | } 157 | 158 | $model->setEventErrorsOnFail($resultObject); 159 | $model->onAfterCreate($result); 160 | $model->onAfterSave($result); 161 | $model->throwExceptionOnFail($resultObject); 162 | 163 | return $model; 164 | } 165 | 166 | /** 167 | * Delete model 168 | * 169 | * @return bool 170 | * @throws ExceptionFromBitrix 171 | */ 172 | public function delete() 173 | { 174 | if ($this->onBeforeDelete() === false) { 175 | return false; 176 | } 177 | 178 | $resultObject = static::instantiateAdapter()->delete($this->id); 179 | $result = $resultObject->isSuccess(); 180 | 181 | $this->setEventErrorsOnFail($resultObject); 182 | $this->onAfterDelete($result); 183 | $this->resetEventErrors(); 184 | $this->throwExceptionOnFail($resultObject); 185 | 186 | return $result; 187 | } 188 | 189 | /** 190 | * Save model to database. 191 | * 192 | * @param array $selectedFields save only these fields instead of all. 193 | * @return bool 194 | * @throws ExceptionFromBitrix 195 | */ 196 | public function save($selectedFields = []) 197 | { 198 | $fieldsSelectedForSave = is_array($selectedFields) ? $selectedFields : func_get_args(); 199 | $this->fieldsSelectedForSave = $fieldsSelectedForSave; 200 | if ($this->onBeforeSave() === false || $this->onBeforeUpdate() === false) { 201 | $this->fieldsSelectedForSave = []; 202 | return false; 203 | } else { 204 | $this->fieldsSelectedForSave = []; 205 | } 206 | 207 | $fields = $this->normalizeFieldsForSave($fieldsSelectedForSave); 208 | $resultObject = $fields === null 209 | ? new UpdateResult() 210 | : static::instantiateAdapter()->update($this->id, $fields); 211 | $result = $resultObject->isSuccess(); 212 | 213 | $this->setEventErrorsOnFail($resultObject); 214 | $this->onAfterUpdate($result); 215 | $this->onAfterSave($result); 216 | $this->throwExceptionOnFail($resultObject); 217 | 218 | return $result; 219 | } 220 | 221 | /** 222 | * Determine whether the field should be stopped from passing to "update". 223 | * 224 | * @param string $field 225 | * @param mixed $value 226 | * @param array $selectedFields 227 | * 228 | * @return bool 229 | */ 230 | protected function fieldShouldNotBeSaved($field, $value, $selectedFields) 231 | { 232 | return (!empty($selectedFields) && !in_array($field, $selectedFields)) || $field === 'ID'; 233 | } 234 | 235 | /** 236 | * Throw bitrix exception on fail 237 | * 238 | * @param \Bitrix\Main\Entity\Result $resultObject 239 | * @throws ExceptionFromBitrix 240 | */ 241 | protected function throwExceptionOnFail($resultObject) 242 | { 243 | if (!$resultObject->isSuccess()) { 244 | throw new ExceptionFromBitrix(implode('; ', $resultObject->getErrorMessages())); 245 | } 246 | } 247 | 248 | /** 249 | * Set eventErrors field on error. 250 | * 251 | * @param \Bitrix\Main\Entity\Result $resultObject 252 | */ 253 | protected function setEventErrorsOnFail($resultObject) 254 | { 255 | if (!$resultObject->isSuccess()) { 256 | $this->eventErrors = (array) $resultObject->getErrorMessages(); 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/Models/ElementModel.php: -------------------------------------------------------------------------------- 1 | add($fields, static::$workFlow, static::$updateSearch, static::$resizePictures); 149 | } 150 | 151 | /** 152 | * Fetches static::$iblockPropertiesData if it's not fetched and returns it. 153 | * 154 | * @return array 155 | */ 156 | protected static function getCachedIblockPropertiesData() 157 | { 158 | $iblockId = static::iblockId(); 159 | if (!empty(self::$iblockPropertiesData[$iblockId])) { 160 | return self::$iblockPropertiesData[$iblockId]; 161 | } 162 | 163 | $props = []; 164 | $dbRes = CIBlock::GetProperties($iblockId, [], []); 165 | while($property = $dbRes->Fetch()) { 166 | $props[$property['CODE']] = $property; 167 | } 168 | 169 | return self::$iblockPropertiesData[$iblockId] = $props; 170 | } 171 | 172 | /** 173 | * Setter for self::$iblockPropertiesData[static::iblockId()] mainly for testing. 174 | * 175 | * @param $data 176 | * @return void 177 | */ 178 | public static function setCachedIblockPropertiesData($data) 179 | { 180 | self::$iblockPropertiesData[static::iblockId()] = $data; 181 | } 182 | 183 | /** 184 | * Corresponding section model full qualified class name. 185 | * MUST be overridden if you are going to use section model for this iblock. 186 | * 187 | * @throws LogicException 188 | * 189 | * @return string 190 | */ 191 | public static function sectionModel() 192 | { 193 | throw new LogicException('public static function sectionModel() MUST be overridden'); 194 | } 195 | 196 | /** 197 | * Instantiate a query object for the model. 198 | * 199 | * @return ElementQuery 200 | */ 201 | public static function query() 202 | { 203 | return new ElementQuery(static::instantiateObject(), get_called_class()); 204 | } 205 | 206 | /** 207 | * Scope to sort by date. 208 | * 209 | * @param ElementQuery $query 210 | * @param string $sort 211 | * 212 | * @return ElementQuery 213 | */ 214 | public function scopeSortByDate($query, $sort = 'DESC') 215 | { 216 | return $query->sort(['ACTIVE_FROM' => $sort]); 217 | } 218 | 219 | /** 220 | * Scope to get only items from a given section. 221 | * 222 | * @param ElementQuery $query 223 | * @param mixed $id 224 | * 225 | * @return ElementQuery 226 | */ 227 | public function scopeFromSectionWithId($query, $id) 228 | { 229 | $query->filter['SECTION_ID'] = $id; 230 | 231 | return $query; 232 | } 233 | 234 | /** 235 | * Scope to get only items from a given section. 236 | * 237 | * @param ElementQuery $query 238 | * @param string $code 239 | * 240 | * @return ElementQuery 241 | */ 242 | public function scopeFromSectionWithCode($query, $code) 243 | { 244 | $query->filter['SECTION_CODE'] = $code; 245 | 246 | return $query; 247 | } 248 | 249 | /** 250 | * Fill extra fields when $this->field is called. 251 | * 252 | * @return null 253 | */ 254 | protected function afterFill() 255 | { 256 | $this->normalizePropertyFormat(); 257 | } 258 | 259 | /** 260 | * Load all model attributes from cache or database. 261 | * 262 | * @return $this 263 | */ 264 | public function load() 265 | { 266 | $this->getFields(); 267 | 268 | return $this; 269 | } 270 | 271 | /** 272 | * Get element's sections from cache or database. 273 | * 274 | * @return array 275 | */ 276 | public function getSections() 277 | { 278 | if ($this->sectionsAreFetched) { 279 | return $this->fields['IBLOCK_SECTION']; 280 | } 281 | 282 | return $this->refreshSections(); 283 | } 284 | 285 | /** 286 | * Refresh model from database and place data to $this->fields. 287 | * 288 | * @return array 289 | */ 290 | public function refresh() 291 | { 292 | return $this->refreshFields(); 293 | } 294 | 295 | /** 296 | * Refresh element's fields and save them to a class field. 297 | * 298 | * @return array 299 | */ 300 | public function refreshFields() 301 | { 302 | if ($this->id === null) { 303 | $this->original = []; 304 | return $this->fields = []; 305 | } 306 | 307 | $sectionsBackup = isset($this->fields['IBLOCK_SECTION']) ? $this->fields['IBLOCK_SECTION'] : null; 308 | 309 | $this->fields = static::query()->getById($this->id)->fields; 310 | 311 | if (!empty($sectionsBackup)) { 312 | $this->fields['IBLOCK_SECTION'] = $sectionsBackup; 313 | } 314 | 315 | $this->fieldsAreFetched = true; 316 | 317 | $this->original = $this->fields; 318 | 319 | return $this->fields; 320 | } 321 | 322 | /** 323 | * Refresh element's sections and save them to a class field. 324 | * 325 | * @return array 326 | */ 327 | public function refreshSections() 328 | { 329 | if ($this->id === null) { 330 | return []; 331 | } 332 | 333 | $this->fields['IBLOCK_SECTION'] = []; 334 | $dbSections = static::$bxObject->getElementGroups($this->id, true); 335 | while ($section = $dbSections->Fetch()) { 336 | $this->fields['IBLOCK_SECTION'][] = $section; 337 | } 338 | 339 | $this->sectionsAreFetched = true; 340 | 341 | return $this->fields['IBLOCK_SECTION']; 342 | } 343 | 344 | /** 345 | * @deprecated in favour of `->section()` 346 | * Get element direct section as ID or array of fields. 347 | * 348 | * @param bool $load 349 | * 350 | * @return false|int|array 351 | */ 352 | public function getSection($load = false) 353 | { 354 | $fields = $this->getFields(); 355 | if (!$load) { 356 | return $fields['IBLOCK_SECTION_ID']; 357 | } 358 | 359 | /** @var SectionModel $sectionModel */ 360 | $sectionModel = static::sectionModel(); 361 | if (!$fields['IBLOCK_SECTION_ID']) { 362 | return false; 363 | } 364 | 365 | return $sectionModel::query()->getById($fields['IBLOCK_SECTION_ID'])->toArray(); 366 | } 367 | 368 | /** 369 | * Get element direct section as model object. 370 | * 371 | * @param bool $load 372 | * 373 | * @return false|SectionModel 374 | */ 375 | public function section($load = false) 376 | { 377 | $fields = $this->getFields(); 378 | 379 | /** @var SectionModel $sectionModel */ 380 | $sectionModel = static::sectionModel(); 381 | 382 | return $load 383 | ? $sectionModel::query()->getById($fields['IBLOCK_SECTION_ID']) 384 | : new $sectionModel($fields['IBLOCK_SECTION_ID']); 385 | } 386 | 387 | /** 388 | * Proxy for GetPanelButtons 389 | * 390 | * @param array $options 391 | * @return array 392 | */ 393 | public function getPanelButtons($options = []) 394 | { 395 | return CIBlock::GetPanelButtons( 396 | static::iblockId(), 397 | $this->id, 398 | 0, 399 | $options 400 | ); 401 | } 402 | 403 | /** 404 | * Save props to database. 405 | * If selected is not empty then only props from it are saved. 406 | * 407 | * @param array $selected 408 | * 409 | * @return bool 410 | */ 411 | public function saveProps($selected = []) 412 | { 413 | $propertyValues = $this->constructPropertyValuesForSave($selected); 414 | if (empty($propertyValues)) { 415 | return false; 416 | } 417 | 418 | $bxMethod = empty($selected) ? 'setPropertyValues' : 'setPropertyValuesEx'; 419 | static::$bxObject->$bxMethod( 420 | $this->id, 421 | static::iblockId(), 422 | $propertyValues 423 | ); 424 | 425 | return true; 426 | } 427 | 428 | /** 429 | * Normalize properties's format converting it to 'PROPERTY_"CODE"_VALUE'. 430 | * 431 | * @return null 432 | */ 433 | protected function normalizePropertyFormat() 434 | { 435 | if (empty($this->fields['PROPERTIES'])) { 436 | return; 437 | } 438 | 439 | foreach ($this->fields['PROPERTIES'] as $code => $prop) { 440 | $this->fields['PROPERTY_'.$code.'_VALUE'] = $prop['VALUE']; 441 | $this->fields['~PROPERTY_'.$code.'_VALUE'] = $prop['~VALUE']; 442 | $this->fields['PROPERTY_'.$code.'_DESCRIPTION'] = $prop['DESCRIPTION']; 443 | $this->fields['~PROPERTY_'.$code.'_DESCRIPTION'] = $prop['~DESCRIPTION']; 444 | $this->fields['PROPERTY_'.$code.'_VALUE_ID'] = $prop['PROPERTY_VALUE_ID']; 445 | } 446 | } 447 | 448 | /** 449 | * Construct 'PROPERTY_VALUES' => [...] from flat fields array. 450 | * This is used in save. 451 | * If $selectedFields are specified only those are saved. 452 | * 453 | * @param $selectedFields 454 | * 455 | * @return array 456 | */ 457 | protected function constructPropertyValuesForSave($selectedFields = []) 458 | { 459 | $propertyValues = []; 460 | $saveOnlySelected = !empty($selectedFields); 461 | 462 | $iblockPropertiesData = static::getCachedIblockPropertiesData(); 463 | 464 | if ($saveOnlySelected) { 465 | foreach ($selectedFields as $code) { 466 | // if we pass PROPERTY_X_DESCRIPTION as selected field, we need to add PROPERTY_X_VALUE as well. 467 | if (preg_match('/^PROPERTY_(.*)_DESCRIPTION$/', $code, $matches) && !empty($matches[1])) { 468 | $propertyCode = $matches[1]; 469 | $propertyValueKey = "PROPERTY_{$propertyCode}_VALUE"; 470 | if (!in_array($propertyValueKey, $selectedFields)) { 471 | $selectedFields[] = $propertyValueKey; 472 | } 473 | } 474 | 475 | // if we pass PROPERTY_X_ENUM_ID as selected field, we need to add PROPERTY_X_VALUE as well. 476 | if (preg_match('/^PROPERTY_(.*)_ENUM_ID$/', $code, $matches) && !empty($matches[1])) { 477 | $propertyCode = $matches[1]; 478 | $propertyValueKey = "PROPERTY_{$propertyCode}_VALUE"; 479 | if (!in_array($propertyValueKey, $selectedFields)) { 480 | $selectedFields[] = $propertyValueKey; 481 | } 482 | } 483 | } 484 | } 485 | 486 | foreach ($this->fields as $code => $value) { 487 | if ($saveOnlySelected && !in_array($code, $selectedFields)) { 488 | continue; 489 | } 490 | 491 | if (preg_match('/^PROPERTY_(.*)_VALUE$/', $code, $matches) && !empty($matches[1])) { 492 | $propertyCode = $matches[1]; 493 | $iblockPropertyData = (array) $iblockPropertiesData[$propertyCode]; 494 | 495 | // if file was not changed skip it or it will be duplicated 496 | if ($iblockPropertyData && $iblockPropertyData['PROPERTY_TYPE'] === 'F' && !empty($this->original[$code]) && $this->original[$code] === $value) { 497 | continue; 498 | } 499 | 500 | // if property type is a list we need to use enum ID/IDs as value/values 501 | if (array_key_exists("PROPERTY_{$propertyCode}_ENUM_ID", $this->fields)) { 502 | $value = $this->fields["PROPERTY_{$propertyCode}_ENUM_ID"]; 503 | } elseif ($iblockPropertyData && $iblockPropertyData['PROPERTY_TYPE'] === 'L' && $iblockPropertyData['MULTIPLE'] === 'Y') { 504 | $value = array_keys($value); 505 | } 506 | 507 | // if property values have descriptions 508 | // we skip file properties here for now because they cause endless problems. Handle them manually. 509 | if (array_key_exists("PROPERTY_{$propertyCode}_DESCRIPTION", $this->fields) && (!$iblockPropertyData || $iblockPropertyData['PROPERTY_TYPE'] !== 'F')) { 510 | $description = $this->fields["PROPERTY_{$propertyCode}_DESCRIPTION"]; 511 | 512 | if (is_array($value) && is_array($description)) { 513 | // for multiple property 514 | foreach ($value as $rowIndex => $rowValue) { 515 | $propertyValues[$propertyCode][] = [ 516 | 'VALUE' => $rowValue, 517 | 'DESCRIPTION' => $description[$rowIndex] 518 | ]; 519 | } 520 | } else { 521 | // for single property 522 | $propertyValues[$propertyCode] = [ 523 | 'VALUE' => $value, 524 | 'DESCRIPTION' => $description 525 | ]; 526 | } 527 | } else { 528 | $propertyValues[$propertyCode] = $value; 529 | } 530 | } 531 | } 532 | 533 | return $propertyValues; 534 | } 535 | 536 | /** 537 | * Determine whether the field should be stopped from passing to "update". 538 | * 539 | * @param string $field 540 | * @param mixed $value 541 | * @param array $selectedFields 542 | * 543 | * @return bool 544 | */ 545 | protected function fieldShouldNotBeSaved($field, $value, $selectedFields) 546 | { 547 | $blacklistedFields = [ 548 | 'ID', 549 | 'IBLOCK_ID', 550 | 'PROPERTIES', 551 | 'PROPERTY_VALUES', 552 | ]; 553 | 554 | return (!empty($selectedFields) && !in_array($field, $selectedFields)) 555 | || in_array($field, $blacklistedFields) 556 | || ($field[0] === '~'); 557 | //|| (substr($field, 0, 9) === 'PROPERTY_'); 558 | } 559 | 560 | /** 561 | * @param $fields 562 | * @param $fieldsSelectedForSave 563 | * @return bool 564 | */ 565 | protected function internalUpdate($fields, $fieldsSelectedForSave) 566 | { 567 | $fields = $fields ?: []; 568 | foreach ($fields as $key => $value) { 569 | if (substr($key, 0, 9) === 'PROPERTY_') { 570 | unset($fields[$key]); 571 | } 572 | } 573 | 574 | $result = !empty($fields) ? static::$bxObject->update($this->id, $fields, static::$workFlow, static::$updateSearch, static::$resizePictures) : false; 575 | $savePropsResult = $this->saveProps($fieldsSelectedForSave); 576 | $result = $result || $savePropsResult; 577 | 578 | return $result; 579 | } 580 | 581 | /** 582 | * Get value from language field according to current language. 583 | * 584 | * @param $field 585 | * @return mixed 586 | */ 587 | protected function getValueFromLanguageField($field) 588 | { 589 | $key = $field . '_' . $this->getCurrentLanguage() . '_VALUE'; 590 | 591 | return isset($this->fields[$key]) ? $this->fields[$key] : null; 592 | } 593 | 594 | /** 595 | * @param $value 596 | */ 597 | public static function setWorkflow($value) 598 | { 599 | static::$workFlow = $value; 600 | } 601 | 602 | /** 603 | * @param $value 604 | */ 605 | public static function setUpdateSearch($value) 606 | { 607 | static::$updateSearch = $value; 608 | } 609 | 610 | /** 611 | * @param $value 612 | */ 613 | public static function setResizePictures($value) 614 | { 615 | static::$resizePictures = $value; 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /src/Models/EloquentModel.php: -------------------------------------------------------------------------------- 1 | multipleHighloadBlockFields)) { 44 | return unserialize($this->attributes[$key]); 45 | } 46 | 47 | return parent::getAttribute($key); 48 | } 49 | 50 | /** 51 | * Set a given attribute on the model. 52 | * 53 | * @param string $key 54 | * @param mixed $value 55 | * @return $this 56 | */ 57 | public function setAttribute($key, $value) 58 | { 59 | if (in_array($key, $this->multipleHighloadBlockFields)) { 60 | $this->attributes[$key] = serialize($value); 61 | 62 | return $this; 63 | } 64 | 65 | parent::setAttribute($key, $value); 66 | 67 | return $this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Models/SectionModel.php: -------------------------------------------------------------------------------- 1 | filter($filter) 145 | ->filter(['SECTION_ID' => $this->id]) 146 | ->select('ID') 147 | ->getList() 148 | ->transform(function ($section) { 149 | return (int) $section['ID']; 150 | }) 151 | ->all(); 152 | } 153 | 154 | /** 155 | * Get IDs of all children of the section (direct or not). 156 | * Additional filter can be specified. 157 | * 158 | * @param array $filter 159 | * @param array|string $sort 160 | * 161 | * @return array 162 | */ 163 | public function getAllChildren(array $filter = [], $sort = ['LEFT_MARGIN' => 'ASC']) 164 | { 165 | if (!isset($this->fields['LEFT_MARGIN']) || !isset($this->fields['RIGHT_MARGIN'])) { 166 | $this->refresh(); 167 | } 168 | 169 | return static::query() 170 | ->sort($sort) 171 | ->filter($filter) 172 | ->filter([ 173 | '!ID' => $this->id, 174 | '>LEFT_MARGIN' => $this->fields['LEFT_MARGIN'], 175 | ' $this->fields['RIGHT_MARGIN'], 176 | ]) 177 | ->select('ID') 178 | ->getList() 179 | ->transform(function ($section) { 180 | return (int) $section['ID']; 181 | }) 182 | ->all(); 183 | } 184 | 185 | /** 186 | * Proxy for GetPanelButtons 187 | * 188 | * @param array $options 189 | * @return array 190 | */ 191 | public function getPanelButtons($options = []) 192 | { 193 | return CIBlock::GetPanelButtons( 194 | static::iblockId(), 195 | 0, 196 | $this->id, 197 | $options 198 | ); 199 | } 200 | 201 | public static function internalDirectCreate($bxObject, $fields) 202 | { 203 | return $bxObject->add($fields, static::$resort, static::$updateSearch, static::$resizePictures); 204 | } 205 | 206 | /** 207 | * @param $fields 208 | * @param $fieldsSelectedForSave 209 | * @return bool 210 | */ 211 | protected function internalUpdate($fields, $fieldsSelectedForSave) 212 | { 213 | return !empty($fields) ? static::$bxObject->update($this->id, $fields, static::$resort, static::$updateSearch, static::$resizePictures) : false; 214 | } 215 | 216 | /** 217 | * @param $value 218 | */ 219 | public static function setResort($value) 220 | { 221 | static::$resort = $value; 222 | } 223 | 224 | /** 225 | * @param $value 226 | */ 227 | public static function setUpdateSearch($value) 228 | { 229 | static::$updateSearch = $value; 230 | } 231 | 232 | /** 233 | * @param $value 234 | */ 235 | public static function setResizePictures($value) 236 | { 237 | static::$resizePictures = $value; 238 | } 239 | 240 | /** 241 | * @param $query 242 | * @param SectionModel $section 243 | * @return SectionQuery 244 | */ 245 | public function scopeChildrenOf(SectionQuery $query, SectionModel $section) 246 | { 247 | $query->filter['>LEFT_MARGIN'] = $section->fields['LEFT_MARGIN']; 248 | $query->filter['fields['RIGHT_MARGIN']; 249 | $query->filter['>DEPTH_LEVEL'] = $section->fields['DEPTH_LEVEL']; 250 | 251 | return $query; 252 | } 253 | 254 | /** 255 | * @param $query 256 | * @param SectionModel|int $section 257 | * @return SectionQuery 258 | */ 259 | public function scopeDirectChildrenOf(SectionQuery $query, $section) 260 | { 261 | $query->filter['SECTION_ID'] = is_int($section) ? $section : $section->id; 262 | 263 | return $query; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Models/Traits/DeactivationTrait.php: -------------------------------------------------------------------------------- 1 | markForActivation()->save(); 16 | } 17 | 18 | /** 19 | * Deactivate element. 20 | */ 21 | public function deactivate() 22 | { 23 | $this->markForDeactivation()->save(); 24 | } 25 | 26 | /** 27 | * @param $query 28 | * @return mixed 29 | */ 30 | public function scopeActive($query) 31 | { 32 | return $this instanceof D7Model 33 | ? $query->filter(['==UF_DEACTIVATED_AT' => null]) 34 | : $query->whereNull('UF_DEACTIVATED_AT'); 35 | } 36 | 37 | /** 38 | * @param $query 39 | * @return mixed 40 | */ 41 | public function scopeDeactivated($query) 42 | { 43 | return $this instanceof D7Model 44 | ? $query->filter(['!==UF_DEACTIVATED_AT' => null]) 45 | : $query->whereNotNull('UF_DEACTIVATED_AT'); 46 | } 47 | 48 | /** 49 | * @return $this 50 | */ 51 | public function markForActivation() 52 | { 53 | $this['UF_DEACTIVATED_AT'] = null; 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * @return $this 60 | */ 61 | public function markForDeactivation() 62 | { 63 | $this['UF_DEACTIVATED_AT'] = $this instanceof D7Model ? new DateTime() : date('Y-m-d H:i:s'); 64 | 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Models/Traits/HidesAttributes.php: -------------------------------------------------------------------------------- 1 | hidden; 29 | } 30 | 31 | /** 32 | * Set the hidden attributes for the model. 33 | * 34 | * @param array $hidden 35 | * @return $this 36 | */ 37 | public function setHidden(array $hidden) 38 | { 39 | $this->hidden = $hidden; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Add hidden attributes for the model. 46 | * 47 | * @param array|string|null $attributes 48 | * @return void 49 | */ 50 | public function addHidden($attributes = null) 51 | { 52 | $this->hidden = array_merge( 53 | $this->hidden, 54 | is_array($attributes) ? $attributes : func_get_args() 55 | ); 56 | } 57 | 58 | /** 59 | * Get the visible attributes for the model. 60 | * 61 | * @return array 62 | */ 63 | public function getVisible() 64 | { 65 | return $this->visible; 66 | } 67 | 68 | /** 69 | * Set the visible attributes for the model. 70 | * 71 | * @param array $visible 72 | * @return $this 73 | */ 74 | public function setVisible(array $visible) 75 | { 76 | $this->visible = $visible; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Add visible attributes for the model. 83 | * 84 | * @param array|string|null $attributes 85 | * @return void 86 | */ 87 | public function addVisible($attributes = null) 88 | { 89 | $this->visible = array_merge( 90 | $this->visible, 91 | is_array($attributes) ? $attributes : func_get_args() 92 | ); 93 | } 94 | 95 | /** 96 | * Make the given, typically hidden, attributes visible. 97 | * 98 | * @param array|string $attributes 99 | * @return $this 100 | */ 101 | public function makeVisible($attributes) 102 | { 103 | $this->hidden = array_diff($this->hidden, (array) $attributes); 104 | 105 | if (!empty($this->visible)) { 106 | $this->addVisible($attributes); 107 | } 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Make the given, typically visible, attributes hidden. 114 | * 115 | * @param array|string $attributes 116 | * @return $this 117 | */ 118 | public function makeHidden($attributes) 119 | { 120 | $attributes = (array) $attributes; 121 | 122 | $this->visible = array_diff($this->visible, $attributes); 123 | 124 | $this->hidden = array_unique(array_merge($this->hidden, $attributes)); 125 | 126 | return $this; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Models/Traits/ModelEventsTrait.php: -------------------------------------------------------------------------------- 1 | getId()))->load(); 101 | } 102 | 103 | /** 104 | * Fill extra fields when $this->field is called. 105 | * 106 | * @return null 107 | */ 108 | protected function afterFill() 109 | { 110 | if (isset($this->fields['GROUP_ID']) && is_array(['GROUP_ID'])) { 111 | $this->groupsAreFetched = true; 112 | } 113 | } 114 | 115 | /** 116 | * Fill model groups if they are already known. 117 | * Saves DB queries. 118 | * 119 | * @param array $groups 120 | * 121 | * @return null 122 | */ 123 | public function fillGroups($groups) 124 | { 125 | $this->fields['GROUP_ID'] = $groups; 126 | 127 | $this->groupsAreFetched = true; 128 | } 129 | 130 | /** 131 | * Load model fields from database if they are not loaded yet. 132 | * 133 | * @return $this 134 | */ 135 | public function load() 136 | { 137 | $this->getFields(); 138 | $this->getGroups(); 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Get user groups from cache or database. 145 | * 146 | * @return array 147 | */ 148 | public function getGroups() 149 | { 150 | if ($this->groupsAreFetched) { 151 | return $this->fields['GROUP_ID']; 152 | } 153 | 154 | return $this->refreshGroups(); 155 | } 156 | 157 | /** 158 | * Refresh model from database and place data to $this->fields. 159 | * 160 | * @return array 161 | */ 162 | public function refresh() 163 | { 164 | $this->refreshFields(); 165 | 166 | $this->refreshGroups(); 167 | 168 | return $this->fields; 169 | } 170 | 171 | /** 172 | * Refresh user fields and save them to a class field. 173 | * 174 | * @return array 175 | */ 176 | public function refreshFields() 177 | { 178 | if ($this->id === null) { 179 | $this->original = []; 180 | return $this->fields = []; 181 | } 182 | 183 | $groupBackup = isset($this->fields['GROUP_ID']) ? $this->fields['GROUP_ID'] : null; 184 | 185 | $this->fields = static::query()->getById($this->id)->fields; 186 | 187 | if ($groupBackup) { 188 | $this->fields['GROUP_ID'] = $groupBackup; 189 | } 190 | 191 | $this->fieldsAreFetched = true; 192 | 193 | $this->original = $this->fields; 194 | 195 | return $this->fields; 196 | } 197 | 198 | /** 199 | * Refresh user groups and save them to a class field. 200 | * 201 | * @return array 202 | */ 203 | public function refreshGroups() 204 | { 205 | if ($this->id === null) { 206 | return []; 207 | } 208 | 209 | global $USER; 210 | 211 | $this->fields['GROUP_ID'] = $this->isCurrent() 212 | ? $USER->getUserGroupArray() 213 | : static::$bxObject->getUserGroup($this->id); 214 | 215 | $this->groupsAreFetched = true; 216 | 217 | return $this->fields['GROUP_ID']; 218 | } 219 | 220 | /** 221 | * Check if user is an admin. 222 | */ 223 | public function isAdmin() 224 | { 225 | return $this->hasGroupWithId(1); 226 | } 227 | 228 | /** 229 | * Check if this user is the operating user. 230 | */ 231 | public function isCurrent() 232 | { 233 | global $USER; 234 | 235 | return $USER->getId() && $this->id == $USER->getId(); 236 | } 237 | 238 | /** 239 | * Check if user has role with a given ID. 240 | * 241 | * @param $role_id 242 | * 243 | * @return bool 244 | */ 245 | public function hasGroupWithId($role_id) 246 | { 247 | return in_array($role_id, $this->getGroups()); 248 | } 249 | 250 | /** 251 | * Check if user is authorized. 252 | * 253 | * @return bool 254 | */ 255 | public function isAuthorized() 256 | { 257 | global $USER; 258 | 259 | return ($USER->getId() == $this->id) && $USER->isAuthorized(); 260 | } 261 | 262 | /** 263 | * Check if user is guest. 264 | * 265 | * @return bool 266 | */ 267 | public function isGuest() 268 | { 269 | return !$this->isAuthorized(); 270 | } 271 | 272 | /** 273 | * Logout user. 274 | * 275 | * @return void 276 | */ 277 | public function logout() 278 | { 279 | global $USER; 280 | 281 | $USER->logout(); 282 | } 283 | 284 | /** 285 | * Scope to get only users from a given group / groups. 286 | * 287 | * @param UserQuery $query 288 | * @param int|array $id 289 | * 290 | * @return UserQuery 291 | */ 292 | public function scopeFromGroup($query, $id) 293 | { 294 | $query->filter['GROUPS_ID'] = $id; 295 | 296 | return $query; 297 | } 298 | 299 | /** 300 | * Substitute old group with the new one. 301 | * 302 | * @param int $old 303 | * @param int $new 304 | * 305 | * @return void 306 | */ 307 | public function substituteGroup($old, $new) 308 | { 309 | $groups = $this->getGroups(); 310 | 311 | if (($key = array_search($old, $groups)) !== false) { 312 | unset($groups[$key]); 313 | } 314 | 315 | if (!in_array($new, $groups)) { 316 | $groups[] = $new; 317 | } 318 | 319 | $this->fields['GROUP_ID'] = $groups; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Queries/BaseQuery.php: -------------------------------------------------------------------------------- 1 | primaryModel)) { 105 | // Запрос - подгрузка релейшена. Надо добавить filter 106 | $this->filterByModels([$this->primaryModel]); 107 | } 108 | 109 | if ($this->queryShouldBeStopped) { 110 | return new Collection(); 111 | } 112 | 113 | $models = $this->loadModels(); 114 | 115 | if (!empty($this->with)) { 116 | $this->findWith($this->with, $models); 117 | } 118 | 119 | return $models; 120 | } 121 | 122 | /** 123 | * Get list of items. 124 | * 125 | * @return Collection 126 | */ 127 | abstract protected function loadModels(); 128 | 129 | /** 130 | * Constructor. 131 | * 132 | * @param object|string $bxObject 133 | * @param string $modelName 134 | */ 135 | public function __construct($bxObject, $modelName) 136 | { 137 | $this->bxObject = $bxObject; 138 | $this->modelName = $modelName; 139 | $this->model = new $modelName(); 140 | } 141 | 142 | /** 143 | * Get the first item that matches query params. 144 | * 145 | * @return mixed 146 | */ 147 | public function first() 148 | { 149 | return $this->limit(1)->getList()->first(null, false); 150 | } 151 | 152 | /** 153 | * Get item by its id. 154 | * 155 | * @param int $id 156 | * 157 | * @return mixed 158 | */ 159 | public function getById($id) 160 | { 161 | if (!$id || $this->queryShouldBeStopped) { 162 | return false; 163 | } 164 | 165 | $this->sort = []; 166 | $this->filter['ID'] = $id; 167 | 168 | return $this->getList()->first(null, false); 169 | } 170 | 171 | /** 172 | * Setter for sort. 173 | * 174 | * @param mixed $by 175 | * @param string $order 176 | * 177 | * @return $this 178 | */ 179 | public function sort($by, $order = 'ASC') 180 | { 181 | $this->sort = is_array($by) ? $by : [$by => $order]; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Another setter for sort. 188 | * 189 | * @param mixed $by 190 | * @param string $order 191 | * 192 | * @return $this 193 | */ 194 | public function order($by, $order = 'ASC') 195 | { 196 | return $this->sort($by, $order); 197 | } 198 | 199 | /** 200 | * Setter for filter. 201 | * 202 | * @param array $filter 203 | * 204 | * @return $this 205 | */ 206 | public function filter($filter) 207 | { 208 | $this->filter = array_merge($this->filter, $filter); 209 | 210 | return $this; 211 | } 212 | 213 | /** 214 | * Reset filter. 215 | * 216 | * @return $this 217 | */ 218 | public function resetFilter() 219 | { 220 | $this->filter = []; 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * Add another filter to filters array. 227 | * 228 | * @param array $filters 229 | * 230 | * @return $this 231 | */ 232 | public function addFilter($filters) 233 | { 234 | foreach ($filters as $field => $value) { 235 | $this->filter[$field] = $value; 236 | } 237 | 238 | return $this; 239 | } 240 | 241 | /** 242 | * Setter for navigation. 243 | * 244 | * @param $value 245 | * 246 | * @return $this 247 | */ 248 | public function navigation($value) 249 | { 250 | $this->navigation = $value; 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Setter for select. 257 | * 258 | * @param $value 259 | * 260 | * @return $this 261 | */ 262 | public function select($value) 263 | { 264 | $this->select = is_array($value) ? $value : func_get_args(); 265 | 266 | return $this; 267 | } 268 | 269 | /** 270 | * Setter for cache ttl. 271 | * 272 | * @param float|int $minutes 273 | * 274 | * @return $this 275 | */ 276 | public function cache($minutes) 277 | { 278 | $this->cacheTtl = $minutes; 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * Setter for keyBy. 285 | * 286 | * @param string $value 287 | * 288 | * @return $this 289 | */ 290 | public function keyBy($value) 291 | { 292 | $this->keyBy = $value; 293 | 294 | return $this; 295 | } 296 | 297 | /** 298 | * Set the "limit" value of the query. 299 | * 300 | * @param int $value 301 | * 302 | * @return $this 303 | */ 304 | public function limit($value) 305 | { 306 | $this->navigation['nPageSize'] = $value; 307 | 308 | return $this; 309 | } 310 | 311 | /** 312 | * Set the "page number" value of the query. 313 | * 314 | * @param int $num 315 | * 316 | * @return $this 317 | */ 318 | public function page($num) 319 | { 320 | $this->navigation['iNumPage'] = $num; 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * Alias for "limit". 327 | * 328 | * @param int $value 329 | * 330 | * @return $this 331 | */ 332 | public function take($value) 333 | { 334 | return $this->limit($value); 335 | } 336 | 337 | /** 338 | * Set the limit and offset for a given page. 339 | * 340 | * @param int $page 341 | * @param int $perPage 342 | * @return $this 343 | */ 344 | public function forPage($page, $perPage = 15) 345 | { 346 | return $this->take($perPage)->page($page); 347 | } 348 | 349 | /** 350 | * Paginate the given query into a paginator. 351 | * 352 | * @param int $perPage 353 | * @param string $pageName 354 | * 355 | * @return \Illuminate\Pagination\LengthAwarePaginator 356 | */ 357 | public function paginate($perPage = 15, $pageName = 'page') 358 | { 359 | $page = Paginator::resolveCurrentPage($pageName); 360 | $total = $this->count(); 361 | $results = $this->forPage($page, $perPage)->getList(); 362 | 363 | return new LengthAwarePaginator($results, $total, $perPage, $page, [ 364 | 'path' => Paginator::resolveCurrentPath(), 365 | 'pageName' => $pageName, 366 | ]); 367 | } 368 | 369 | /** 370 | * Get a paginator only supporting simple next and previous links. 371 | * 372 | * This is more efficient on larger data-sets, etc. 373 | * 374 | * @param int $perPage 375 | * @param string $pageName 376 | * 377 | * @return \Illuminate\Pagination\Paginator 378 | */ 379 | public function simplePaginate($perPage = 15, $pageName = 'page') 380 | { 381 | $page = Paginator::resolveCurrentPage($pageName); 382 | $results = $this->forPage($page, $perPage + 1)->getList(); 383 | 384 | return new Paginator($results, $perPage, $page, [ 385 | 'path' => Paginator::resolveCurrentPath(), 386 | 'pageName' => $pageName, 387 | ]); 388 | } 389 | 390 | /** 391 | * Stop the query from touching DB. 392 | * 393 | * @return $this 394 | */ 395 | public function stopQuery() 396 | { 397 | $this->queryShouldBeStopped = true; 398 | 399 | return $this; 400 | } 401 | 402 | /** 403 | * Adds $item to $results using keyBy value. 404 | * 405 | * @param $results 406 | * @param BaseBitrixModel $object 407 | * 408 | * @return void 409 | */ 410 | protected function addItemToResultsUsingKeyBy(&$results, BaseBitrixModel $object) 411 | { 412 | $item = $object->fields; 413 | if (!array_key_exists($this->keyBy, $item)) { 414 | throw new LogicException("Field {$this->keyBy} is not found in object"); 415 | } 416 | 417 | $keyByValue = $item[$this->keyBy]; 418 | 419 | if (!isset($results[$keyByValue])) { 420 | $results[$keyByValue] = $object; 421 | } else { 422 | $oldFields = $results[$keyByValue]->fields; 423 | foreach ($oldFields as $field => $oldValue) { 424 | // пропускаем служебные поля. 425 | if (in_array($field, ['_were_multiplied', 'PROPERTIES'])) { 426 | continue; 427 | } 428 | 429 | $alreadyMultiplied = !empty($oldFields['_were_multiplied'][$field]); 430 | 431 | // мультиплицируем только несовпадающие значения полей 432 | $newValue = $item[$field]; 433 | if ($oldValue !== $newValue) { 434 | // если еще не мультиплицировали поле, то его надо превратить в массив. 435 | if (!$alreadyMultiplied) { 436 | $oldFields[$field] = [ 437 | $oldFields[$field] 438 | ]; 439 | $oldFields['_were_multiplied'][$field] = true; 440 | } 441 | 442 | // добавляем новое значению поле если такого еще нет. 443 | if (empty($oldFields[$field]) || (is_array($oldFields[$field]) && !in_array($newValue, $oldFields[$field]))) { 444 | $oldFields[$field][] = $newValue; 445 | } 446 | } 447 | } 448 | 449 | $results[$keyByValue]->fields = $oldFields; 450 | } 451 | } 452 | 453 | /** 454 | * Determine if all fields must be selected. 455 | * 456 | * @return bool 457 | */ 458 | protected function fieldsMustBeSelected() 459 | { 460 | return in_array('FIELDS', $this->select); 461 | } 462 | 463 | /** 464 | * Determine if all fields must be selected. 465 | * 466 | * @return bool 467 | */ 468 | protected function propsMustBeSelected() 469 | { 470 | return in_array('PROPS', $this->select) 471 | || in_array('PROPERTIES', $this->select) 472 | || in_array('PROPERTY_VALUES', $this->select); 473 | } 474 | 475 | /** 476 | * Set $array[$new] as $array[$old] and delete $array[$old]. 477 | * 478 | * @param array $array 479 | * @param $old 480 | * @param $new 481 | * 482 | * return null 483 | */ 484 | protected function substituteField(&$array, $old, $new) 485 | { 486 | if (isset($array[$old]) && !isset($array[$new])) { 487 | $array[$new] = $array[$old]; 488 | } 489 | 490 | unset($array[$old]); 491 | } 492 | 493 | /** 494 | * Clear select array from duplication and additional fields. 495 | * 496 | * @return array 497 | */ 498 | protected function clearSelectArray() 499 | { 500 | $strip = ['FIELDS', 'PROPS', 'PROPERTIES', 'PROPERTY_VALUES', 'GROUPS', 'GROUP_ID', 'GROUPS_ID']; 501 | 502 | return array_values(array_diff(array_unique($this->select), $strip)); 503 | } 504 | 505 | /** 506 | * Store closure's result in the cache for a given number of minutes. 507 | * 508 | * @param string $key 509 | * @param double $minutes 510 | * @param Closure $callback 511 | * @return mixed 512 | */ 513 | protected function rememberInCache($key, $minutes, Closure $callback) 514 | { 515 | $minutes = (float) $minutes; 516 | if ($minutes <= 0) { 517 | return $callback(); 518 | } 519 | 520 | $cache = Cache::createInstance(); 521 | if ($cache->initCache($minutes * 60, $key, '/bitrix-models')) { 522 | $vars = $cache->getVars(); 523 | return !empty($vars['isCollection']) ? new Collection($vars['cache']) : $vars['cache']; 524 | } 525 | 526 | $cache->startDataCache(); 527 | $result = $callback(); 528 | 529 | // Bitrix cache is bad for storing collections. Let's convert it to array. 530 | $isCollection = $result instanceof Collection; 531 | if ($isCollection) { 532 | $result = $result->all(); 533 | } 534 | 535 | $cache->endDataCache(['cache' => $result, 'isCollection' => $isCollection]); 536 | 537 | return $isCollection ? new Collection($result) : $result; 538 | } 539 | 540 | protected function handleCacheIfNeeded($cacheKeyParams, Closure $callback) 541 | { 542 | return $this->cacheTtl 543 | ? $this->rememberInCache(md5(json_encode($cacheKeyParams)), $this->cacheTtl, $callback) 544 | : $callback(); 545 | } 546 | 547 | /** 548 | * Handle dynamic method calls into the method. 549 | * 550 | * @param string $method 551 | * @param array $parameters 552 | * 553 | * @throws BadMethodCallException 554 | * 555 | * @return $this 556 | */ 557 | public function __call($method, $parameters) 558 | { 559 | if (method_exists($this->model, 'scope' . $method)) { 560 | array_unshift($parameters, $this); 561 | 562 | $query = call_user_func_array([$this->model, 'scope' . $method], $parameters); 563 | 564 | if ($query === false) { 565 | $this->stopQuery(); 566 | } 567 | 568 | return $query instanceof static ? $query : $this; 569 | } 570 | 571 | $className = get_class($this); 572 | 573 | throw new BadMethodCallException("Call to undefined method {$className}::{$method}()"); 574 | } 575 | 576 | protected function prepareMultiFilter(&$key, &$value) 577 | { 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /src/Queries/BaseRelationQuery.php: -------------------------------------------------------------------------------- 1 | primaryModel]] 43 | * Этот метод вызывается когда релейшн вызывается ленивой загрузкой $model->relation 44 | * @return Collection|BaseBitrixModel[]|BaseBitrixModel - связанные модели 45 | * @throws \Exception 46 | */ 47 | public function findFor() 48 | { 49 | return $this->multiple ? $this->getList() : $this->first(); 50 | } 51 | 52 | /** 53 | * Определяет связи, которые должны быть загружены при выполнении запроса 54 | * 55 | * Передавая массив можно указать ключем - название релейшена, а значением - коллбек для кастомизации запроса 56 | * 57 | * @param array|string $with - связи, которые необходимо жадно подгрузить 58 | * // Загрузить Customer и сразу для каждой модели подгрузить orders и country 59 | * Customer::query()->with(['orders', 'country'])->getList(); 60 | * 61 | * // Загрузить Customer и сразу для каждой модели подгрузить orders, а также для orders загрузить address 62 | * Customer::find()->with('orders.address')->getList(); 63 | * 64 | * // Загрузить Customer и сразу для каждой модели подгрузить country и orders (только активные) 65 | * Customer::find()->with([ 66 | * 'orders' => function (BaseQuery $query) { 67 | * $query->filter(['ACTIVE' => 'Y']); 68 | * }, 69 | * 'country', 70 | * ])->all(); 71 | * 72 | * @return $this 73 | */ 74 | public function with($with) 75 | { 76 | $with = is_string($with) ? func_get_args() : $with; 77 | 78 | if (empty($this->with)) { 79 | $this->with = $with; 80 | } elseif (!empty($with)) { 81 | foreach ($with as $name => $value) { 82 | if (is_int($name)) { 83 | // дубликаты связей будут устранены в normalizeRelations() 84 | $this->with[] = $value; 85 | } else { 86 | $this->with[$name] = $value; 87 | } 88 | } 89 | } 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Добавить фильтр для загрзуки связи относительно моделей 96 | * @param Collection|BaseBitrixModel[] $models 97 | */ 98 | protected function filterByModels($models) 99 | { 100 | $values = []; 101 | foreach ($models as $model) { 102 | if (($value = $model[$this->foreignKey]) !== null) { 103 | if (is_array($value)) { 104 | $values = array_merge($values, $value); 105 | } else { 106 | $values[] = $value; 107 | } 108 | } 109 | } 110 | 111 | $values = array_filter($values); 112 | if (empty($values)) { 113 | $this->stopQuery(); 114 | } 115 | 116 | $primary = $this->localKey; 117 | if (preg_match('/^PROPERTY_(.*)_VALUE$/', $primary, $matches) && !empty($matches[1])) { 118 | $primary = 'PROPERTY_' . $matches[1]; 119 | } 120 | $values = array_unique($values, SORT_REGULAR); 121 | if (count($values) == 1) { 122 | $values = current($values); 123 | } else { 124 | $this->prepareMultiFilter($primary, $values); 125 | } 126 | 127 | $this->filter([$primary => $values]); 128 | $this->select[] = $primary; 129 | } 130 | 131 | /** 132 | * Подгрузить связанные модели для уже загруденных моделей 133 | * @param array $with - массив релейшенов, которые необходимо подгрузить 134 | * @param Collection|BaseBitrixModel[] $models модели, для которых загружать связи 135 | */ 136 | public function findWith($with, &$models) 137 | { 138 | // --- получаем модель, на основании которой будем брать запросы релейшенов 139 | $primaryModel = $models->first(); 140 | if (!$primaryModel instanceof BaseBitrixModel) { 141 | $primaryModel = $this->model; 142 | } 143 | 144 | $relations = $this->normalizeRelations($primaryModel, $with); 145 | /* @var $relation BaseQuery */ 146 | foreach ($relations as $name => $relation) { 147 | $relation->populateRelation($name, $models); 148 | } 149 | } 150 | 151 | /** 152 | * @param BaseBitrixModel $model - модель пустышка, чтобы получить запросы 153 | * @param array $with 154 | * @return BaseQuery[] 155 | */ 156 | private function normalizeRelations($model, $with) 157 | { 158 | $relations = []; 159 | foreach ($with as $name => $callback) { 160 | if (is_int($name)) { // Если ключ - число, значит в значении написано название релейшена 161 | $name = $callback; 162 | $callback = null; 163 | } 164 | 165 | if (($pos = strpos($name, '.')) !== false) { // Если есть точка, значит указан вложенный релейшн 166 | $childName = substr($name, $pos + 1); // Название дочернего релейшена 167 | $name = substr($name, 0, $pos); // Название текущего релейшена 168 | } else { 169 | $childName = null; 170 | } 171 | 172 | if (!isset($relations[$name])) { // Указываем новый релейшн 173 | $relation = $model->getRelation($name); // Берем запрос 174 | $relation->primaryModel = null; 175 | $relations[$name] = $relation; 176 | } else { 177 | $relation = $relations[$name]; 178 | } 179 | 180 | if (isset($childName)) { 181 | $relation->with[$childName] = $callback; 182 | } elseif ($callback !== null) { 183 | call_user_func($callback, $relation); 184 | } 185 | } 186 | 187 | return $relations; 188 | } 189 | /** 190 | * Находит связанные записи и заполняет их в первичных моделях. 191 | * @param string $name - имя релейшена 192 | * @param array $primaryModels - первичные модели 193 | * @return Collection|BaseBitrixModel[] - найденные модели 194 | */ 195 | public function populateRelation($name, &$primaryModels) 196 | { 197 | $this->filterByModels($primaryModels); 198 | 199 | $models = $this->getList(); 200 | 201 | Helpers::assocModels($primaryModels, $models, $this->foreignKey, $this->localKey, $name, $this->multiple); 202 | 203 | return $models; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Queries/D7Query.php: -------------------------------------------------------------------------------- 1 | bxObject->getClassName(); 74 | $queryType = 'D7Query::count'; 75 | $filter = $this->filter; 76 | 77 | $callback = function () use ($filter) { 78 | return (int) $this->bxObject->getCount($filter); 79 | }; 80 | 81 | return $this->handleCacheIfNeeded(compact('className', 'filter', 'queryType'), $callback); 82 | } 83 | 84 | /** 85 | * Get list of items. 86 | * 87 | * @return Collection 88 | */ 89 | protected function loadModels() 90 | { 91 | $params = [ 92 | 'select' => $this->select, 93 | 'filter' => $this->filter, 94 | 'group' => $this->group, 95 | 'order' => $this->sort, 96 | 'limit' => $this->limit, 97 | 'offset' => $this->offset, 98 | 'runtime' => $this->runtime, 99 | ]; 100 | 101 | if ($this->cacheTtl && $this->cacheJoins) { 102 | $params['cache'] = ['ttl' => $this->cacheTtl, 'cache_joins' => true]; 103 | } 104 | 105 | $className = $this->bxObject->getClassName(); 106 | $queryType = 'D7Query::getList'; 107 | $keyBy = $this->keyBy; 108 | 109 | $callback = function () use ($className, $params) { 110 | $rows = []; 111 | $result = $this->bxObject->getList($params); 112 | while ($row = $result->fetch()) { 113 | $this->addItemToResultsUsingKeyBy($rows, new $this->modelName($row['ID'], $row)); 114 | } 115 | 116 | return new Collection($rows); 117 | }; 118 | 119 | return $this->handleCacheIfNeeded(compact('className', 'params', 'queryType', 'keyBy'), $callback); 120 | } 121 | 122 | /** 123 | * Setter for limit. 124 | * 125 | * @param int|null $value 126 | * @return $this 127 | */ 128 | public function limit($value) 129 | { 130 | $this->limit = $value; 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Setter for offset. 137 | * 138 | * @param int|null $value 139 | * @return $this 140 | */ 141 | public function offset($value) 142 | { 143 | $this->offset = $value; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Set the "page number" value of the query. 150 | * 151 | * @param int $num 152 | * @return $this 153 | */ 154 | public function page($num) 155 | { 156 | return $this->offset((int) $this->limit * ($num - 1)); 157 | } 158 | 159 | /** 160 | * Setter for offset. 161 | * 162 | * @param array|\Bitrix\Main\Entity\ExpressionField $fields 163 | * @return $this 164 | */ 165 | public function runtime($fields) 166 | { 167 | $this->runtime = is_array($fields) ? $fields : [$fields]; 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Setter for cacheJoins. 174 | * 175 | * @param bool $value 176 | * @return $this 177 | */ 178 | public function cacheJoins($value = true) 179 | { 180 | $this->cacheJoins = $value; 181 | 182 | return $this; 183 | } 184 | 185 | public function enableDataDoubling() 186 | { 187 | $this->dataDoubling = true; 188 | 189 | return $this; 190 | } 191 | 192 | public function disableDataDoubling() 193 | { 194 | $this->dataDoubling = false; 195 | 196 | return $this; 197 | } 198 | 199 | /** 200 | * For testing. 201 | * 202 | * @param $bxObject 203 | * @return $this 204 | */ 205 | public function setAdapter($bxObject) 206 | { 207 | $this->bxObject = $bxObject; 208 | 209 | return $this; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Queries/ElementQuery.php: -------------------------------------------------------------------------------- 1 | 'ASC']; 32 | 33 | /** 34 | * Query group by. 35 | * 36 | * @var array 37 | */ 38 | public $groupBy = false; 39 | 40 | /** 41 | * Iblock id. 42 | * 43 | * @var int 44 | */ 45 | protected $iblockId; 46 | 47 | /** 48 | * Iblock version. 49 | * 50 | * @var int 51 | */ 52 | protected $iblockVersion; 53 | 54 | /** 55 | * List of standard entity fields. 56 | * 57 | * @var array 58 | */ 59 | protected $standardFields = [ 60 | 'ID', 61 | 'TIMESTAMP_X', 62 | 'TIMESTAMP_X_UNIX', 63 | 'MODIFIED_BY', 64 | 'DATE_CREATE', 65 | 'DATE_CREATE_UNIX', 66 | 'CREATED_BY', 67 | 'IBLOCK_ID', 68 | 'IBLOCK_SECTION_ID', 69 | 'ACTIVE', 70 | 'ACTIVE_FROM', 71 | 'ACTIVE_TO', 72 | 'SORT', 73 | 'NAME', 74 | 'PREVIEW_PICTURE', 75 | 'PREVIEW_TEXT', 76 | 'PREVIEW_TEXT_TYPE', 77 | 'DETAIL_PICTURE', 78 | 'DETAIL_TEXT', 79 | 'DETAIL_TEXT_TYPE', 80 | 'SEARCHABLE_CONTENT', 81 | 'IN_SECTIONS', 82 | 'SHOW_COUNTER', 83 | 'SHOW_COUNTER_START', 84 | 'CODE', 85 | 'TAGS', 86 | 'XML_ID', 87 | 'EXTERNAL_ID', 88 | 'TMP_ID', 89 | 'CREATED_USER_NAME', 90 | 'DETAIL_PAGE_URL', 91 | 'LIST_PAGE_URL', 92 | 'CREATED_DATE', 93 | ]; 94 | 95 | /** 96 | * Constructor. 97 | * 98 | * @param object $bxObject 99 | * @param string $modelName 100 | */ 101 | public function __construct($bxObject, $modelName) 102 | { 103 | static::instantiateCIblockObject(); 104 | parent::__construct($bxObject, $modelName); 105 | 106 | $this->iblockId = $modelName::iblockId(); 107 | $this->iblockVersion = $modelName::IBLOCK_VERSION ?: 2; 108 | } 109 | 110 | /** 111 | * Instantiate bitrix entity object. 112 | * 113 | * @throws Exception 114 | * 115 | * @return object 116 | */ 117 | public static function instantiateCIblockObject() 118 | { 119 | if (static::$cIblockObject) { 120 | return static::$cIblockObject; 121 | } 122 | 123 | if (class_exists('CIBlock')) { 124 | return static::$cIblockObject = new CIBlock(); 125 | } 126 | 127 | throw new Exception('CIblock object initialization failed'); 128 | } 129 | 130 | /** 131 | * Setter for groupBy. 132 | * 133 | * @param $value 134 | * 135 | * @return $this 136 | */ 137 | public function groupBy($value) 138 | { 139 | $this->groupBy = $value; 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * Get list of items. 146 | * 147 | * @return Collection 148 | */ 149 | protected function loadModels() 150 | { 151 | $sort = $this->sort; 152 | $filter = $this->normalizeFilter(); 153 | $groupBy = $this->groupBy; 154 | $navigation = $this->navigation; 155 | $select = $this->normalizeSelect(); 156 | $queryType = 'ElementQuery::getList'; 157 | $fetchUsing = $this->fetchUsing; 158 | $keyBy = $this->keyBy; 159 | list($select, $chunkQuery) = $this->multiplySelectForMaxJoinsRestrictionIfNeeded($select); 160 | 161 | $callback = function () use ($sort, $filter, $groupBy, $navigation, $select, $chunkQuery) { 162 | if ($chunkQuery) { 163 | $itemsChunks = []; 164 | foreach ($select as $chunkIndex => $selectForChunk) { 165 | $rsItems = $this->bxObject->GetList($sort, $filter, $groupBy, $navigation, $selectForChunk); 166 | while ($arItem = $this->performFetchUsingSelectedMethod($rsItems)) { 167 | $this->addItemToResultsUsingKeyBy($itemsChunks[$chunkIndex], new $this->modelName($arItem['ID'], $arItem)); 168 | } 169 | } 170 | 171 | $items = $this->mergeChunks($itemsChunks); 172 | } else { 173 | $items = []; 174 | $rsItems = $this->bxObject->GetList($sort, $filter, $groupBy, $navigation, $select); 175 | while ($arItem = $this->performFetchUsingSelectedMethod($rsItems)) { 176 | $this->addItemToResultsUsingKeyBy($items, new $this->modelName($arItem['ID'], $arItem)); 177 | } 178 | } 179 | return new Collection($items); 180 | }; 181 | 182 | $cacheKeyParams = compact('sort', 'filter', 'groupBy', 'navigation', 'select', 'queryType', 'keyBy', 'fetchUsing'); 183 | 184 | return $this->handleCacheIfNeeded($cacheKeyParams, $callback); 185 | } 186 | 187 | /** 188 | * Get the first element with a given code. 189 | * 190 | * @param string $code 191 | * 192 | * @return ElementModel 193 | */ 194 | public function getByCode($code) 195 | { 196 | $this->filter['=CODE'] = $code; 197 | 198 | return $this->first(); 199 | } 200 | 201 | /** 202 | * Get the first element with a given external id. 203 | * 204 | * @param string $id 205 | * 206 | * @return ElementModel 207 | */ 208 | public function getByExternalId($id) 209 | { 210 | $this->filter['EXTERNAL_ID'] = $id; 211 | 212 | return $this->first(); 213 | } 214 | 215 | /** 216 | * Get count of elements that match $filter. 217 | * 218 | * @return int 219 | */ 220 | public function count() 221 | { 222 | if ($this->queryShouldBeStopped) { 223 | return 0; 224 | } 225 | 226 | $filter = $this->normalizeFilter(); 227 | $queryType = "ElementQuery::count"; 228 | 229 | $callback = function () use ($filter) { 230 | return (int) $this->bxObject->GetList(false, $filter, []); 231 | }; 232 | 233 | return $this->handleCacheIfNeeded(compact('filter', 'queryType'), $callback); 234 | } 235 | 236 | // /** 237 | // * Normalize properties's format converting it to 'PROPERTY_"CODE"_VALUE'. 238 | // * 239 | // * @param array $fields 240 | // * 241 | // * @return null 242 | // */ 243 | // protected function normalizePropertyResultFormat(&$fields) 244 | // { 245 | // if (empty($fields['PROPERTIES'])) { 246 | // return; 247 | // } 248 | // 249 | // foreach ($fields['PROPERTIES'] as $code => $prop) { 250 | // $fields['PROPERTY_'.$code.'_VALUE'] = $prop['VALUE']; 251 | // $fields['~PROPERTY_'.$code.'_VALUE'] = $prop['~VALUE']; 252 | // $fields['PROPERTY_'.$code.'_DESCRIPTION'] = $prop['DESCRIPTION']; 253 | // $fields['~PROPERTY_'.$code.'_DESCRIPTION'] = $prop['~DESCRIPTION']; 254 | // $fields['PROPERTY_'.$code.'_VALUE_ID'] = $prop['PROPERTY_VALUE_ID']; 255 | // if (isset($prop['VALUE_ENUM_ID'])) { 256 | // $fields['PROPERTY_'.$code.'_ENUM_ID'] = $prop['VALUE_ENUM_ID']; 257 | // } 258 | // } 259 | // } 260 | 261 | /** 262 | * Normalize filter before sending it to getList. 263 | * This prevents some inconsistency. 264 | * 265 | * @return array 266 | */ 267 | protected function normalizeFilter() 268 | { 269 | $this->filter['IBLOCK_ID'] = $this->iblockId; 270 | 271 | return $this->filter; 272 | } 273 | 274 | /** 275 | * Normalize select before sending it to getList. 276 | * This prevents some inconsistency. 277 | * 278 | * @return array 279 | */ 280 | protected function normalizeSelect() 281 | { 282 | if ($this->fieldsMustBeSelected()) { 283 | $this->select = array_merge($this->standardFields, $this->select); 284 | } 285 | 286 | $this->select[] = 'ID'; 287 | $this->select[] = 'IBLOCK_ID'; 288 | 289 | return $this->clearSelectArray(); 290 | } 291 | 292 | /** 293 | * Fetch all iblock property codes from database 294 | * 295 | * return array 296 | */ 297 | protected function fetchAllPropsForSelect() 298 | { 299 | $props = []; 300 | $rsProps = static::$cIblockObject->GetProperties($this->iblockId); 301 | while ($prop = $rsProps->Fetch()) { 302 | $props[] = 'PROPERTY_' . $prop['CODE']; 303 | } 304 | 305 | return $props; 306 | } 307 | 308 | protected function multiplySelectForMaxJoinsRestrictionIfNeeded($select) 309 | { 310 | if (!$this->propsMustBeSelected()) { 311 | return [$select, false]; 312 | } 313 | 314 | $chunkSize = 20; 315 | $props = $this->fetchAllPropsForSelect(); 316 | if ($this->iblockVersion !== 1 || (count($props) <= $chunkSize)) { 317 | return [array_merge($select, $props), false]; 318 | } 319 | 320 | // начинаем формировать селекты из свойств 321 | $multipleSelect = array_chunk($props, $chunkSize); 322 | 323 | // добавляем в каждый селект поля "несвойства" 324 | foreach ($multipleSelect as $i => $partOfProps) { 325 | $multipleSelect[$i] = array_merge($select, $partOfProps); 326 | } 327 | 328 | return [$multipleSelect, true]; 329 | } 330 | 331 | protected function mergeChunks($chunks) 332 | { 333 | $items = []; 334 | foreach ($chunks as $chunk) { 335 | foreach ($chunk as $k => $item) { 336 | if (isset($items[$k])) { 337 | $item->fields['_were_multiplied'] = array_merge((array) $items[$k]->fields['_were_multiplied'], (array) $item->fields['_were_multiplied']); 338 | $items[$k]->fields = (array) $item->fields + (array) $items[$k]->fields; 339 | } else { 340 | $items[$k] = $item; 341 | } 342 | } 343 | } 344 | 345 | return $items; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Queries/OldCoreQuery.php: -------------------------------------------------------------------------------- 1 | fetchUsing($modelName::$fetchUsing); 32 | } 33 | 34 | /** 35 | * Set fetch using from string or array. 36 | * 37 | * @param string|array $methodAndParams 38 | * @return $this 39 | */ 40 | public function fetchUsing($methodAndParams) 41 | { 42 | // simple case 43 | if (is_string($methodAndParams) || empty($methodAndParams['method'])) { 44 | $this->fetchUsing = in_array($methodAndParams, ['GetNext', 'getNext']) 45 | ? ['method' => 'GetNext', 'params' => [true, true]] 46 | : ['method' => 'Fetch']; 47 | 48 | return $this; 49 | } 50 | 51 | // complex case 52 | if (in_array($methodAndParams['method'], ['GetNext', 'getNext'])) { 53 | $bTextHtmlAuto = isset($methodAndParams['params'][0]) ? $methodAndParams['params'][0] : true; 54 | $useTilda = isset($methodAndParams['params'][1]) ? $methodAndParams['params'][1] : true; 55 | $this->fetchUsing = ['method' => 'GetNext', 'params' => [$bTextHtmlAuto, $useTilda]]; 56 | } else { 57 | $this->fetchUsing = ['method' => 'Fetch']; 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Choose between Fetch() and GetNext($bTextHtmlAuto, $useTilda) and then fetch 65 | * 66 | * @param \CDBResult $rsItems 67 | * @return array|false 68 | */ 69 | protected function performFetchUsingSelectedMethod($rsItems) 70 | { 71 | return $this->fetchUsing['method'] === 'GetNext' 72 | ? $rsItems->GetNext($this->fetchUsing['params'][0], $this->fetchUsing['params'][1]) 73 | : $rsItems->Fetch(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Queries/SectionQuery.php: -------------------------------------------------------------------------------- 1 | 'ASC']; 20 | 21 | /** 22 | * Query bIncCnt. 23 | * This is sent to getList directly. 24 | * 25 | * @var array|false 26 | */ 27 | public $countElements = false; 28 | 29 | /** 30 | * Iblock id. 31 | * 32 | * @var int 33 | */ 34 | protected $iblockId; 35 | 36 | /** 37 | * List of standard entity fields. 38 | * 39 | * @var array 40 | */ 41 | protected $standardFields = [ 42 | 'ID', 43 | 'CODE', 44 | 'EXTERNAL_ID', 45 | 'IBLOCK_ID', 46 | 'IBLOCK_SECTION_ID', 47 | 'TIMESTAMP_X', 48 | 'SORT', 49 | 'NAME', 50 | 'ACTIVE', 51 | 'GLOBAL_ACTIVE', 52 | 'PICTURE', 53 | 'DESCRIPTION', 54 | 'DESCRIPTION_TYPE', 55 | 'LEFT_MARGIN', 56 | 'RIGHT_MARGIN', 57 | 'DEPTH_LEVEL', 58 | 'SEARCHABLE_CONTENT', 59 | 'SECTION_PAGE_URL', 60 | 'MODIFIED_BY', 61 | 'DATE_CREATE', 62 | 'CREATED_BY', 63 | 'DETAIL_PICTURE', 64 | ]; 65 | 66 | /** 67 | * Constructor. 68 | * 69 | * @param object $bxObject 70 | * @param string $modelName 71 | */ 72 | public function __construct($bxObject, $modelName) 73 | { 74 | parent::__construct($bxObject, $modelName); 75 | 76 | $this->iblockId = $modelName::iblockId(); 77 | } 78 | 79 | /** 80 | * CIBlockSection::getList substitution. 81 | * 82 | * @return Collection 83 | */ 84 | protected function loadModels() 85 | { 86 | $queryType = 'SectionQuery::getList'; 87 | $sort = $this->sort; 88 | $filter = $this->normalizeFilter(); 89 | $countElements = $this->countElements; 90 | $select = $this->normalizeSelect(); 91 | $navigation = $this->navigation; 92 | $keyBy = $this->keyBy; 93 | 94 | $callback = function () use ($sort, $filter, $countElements, $select, $navigation) { 95 | $sections = []; 96 | $rsSections = $this->bxObject->getList($sort, $filter, $countElements, $select, $navigation); 97 | while ($arSection = $this->performFetchUsingSelectedMethod($rsSections)) { 98 | 99 | // Если передать nPageSize, то Битрикс почему-то перестает десериализовать множественные свойсвта... 100 | // Проверим это еще раз, и если есть проблемы то пофиксим. 101 | foreach ($arSection as $field => $value) { 102 | if ( 103 | is_string($value) 104 | && Helpers::startsWith($value, 'a:') 105 | && (Helpers::startsWith($field, 'UF_') || Helpers::startsWith($field, '~UF_')) 106 | ) { 107 | $unserializedValue = @unserialize($value); 108 | $arSection[$field] = $unserializedValue === false ? $value : $unserializedValue; 109 | } 110 | } 111 | 112 | $this->addItemToResultsUsingKeyBy($sections, new $this->modelName($arSection['ID'], $arSection)); 113 | } 114 | 115 | return new Collection($sections); 116 | }; 117 | 118 | $cacheParams = compact('queryType', 'sort', 'filter', 'countElements', 'select', 'navigation', 'keyBy'); 119 | 120 | return $this->handleCacheIfNeeded($cacheParams, $callback); 121 | } 122 | 123 | /** 124 | * Get the first section with a given code. 125 | * 126 | * @param string $code 127 | * 128 | * @return SectionModel 129 | */ 130 | public function getByCode($code) 131 | { 132 | $this->filter['=CODE'] = $code; 133 | 134 | return $this->first(); 135 | } 136 | 137 | /** 138 | * Get the first section with a given external id. 139 | * 140 | * @param string $id 141 | * 142 | * @return SectionModel 143 | */ 144 | public function getByExternalId($id) 145 | { 146 | $this->filter['EXTERNAL_ID'] = $id; 147 | 148 | return $this->first(); 149 | } 150 | 151 | /** 152 | * Get count of sections that match filter. 153 | * 154 | * @return int 155 | */ 156 | public function count() 157 | { 158 | if ($this->queryShouldBeStopped) { 159 | return 0; 160 | } 161 | 162 | $queryType = 'SectionQuery::count'; 163 | $filter = $this->normalizeFilter(); 164 | $callback = function () use ($filter) { 165 | return (int) $this->bxObject->getCount($filter); 166 | }; 167 | 168 | return $this->handleCacheIfNeeded(compact('queryType', 'filter'), $callback); 169 | } 170 | 171 | /** 172 | * Setter for countElements. 173 | * 174 | * @param $value 175 | * 176 | * @return $this 177 | */ 178 | public function countElements($value) 179 | { 180 | $this->countElements = $value; 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * Normalize filter before sending it to getList. 187 | * This prevents some inconsistency. 188 | * 189 | * @return array 190 | */ 191 | protected function normalizeFilter() 192 | { 193 | $this->filter['IBLOCK_ID'] = $this->iblockId; 194 | 195 | return $this->filter; 196 | } 197 | 198 | /** 199 | * Normalize select before sending it to getList. 200 | * This prevents some inconsistency. 201 | * 202 | * @return array 203 | */ 204 | protected function normalizeSelect() 205 | { 206 | if ($this->fieldsMustBeSelected()) { 207 | $this->select = array_merge($this->standardFields, $this->select); 208 | } 209 | 210 | if ($this->propsMustBeSelected()) { 211 | $this->select[] = 'IBLOCK_ID'; 212 | $this->select[] = 'UF_*'; 213 | } 214 | 215 | $this->select[] = 'ID'; 216 | 217 | return $this->clearSelectArray(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Queries/UserQuery.php: -------------------------------------------------------------------------------- 1 | 'asc']; 20 | 21 | /** 22 | * List of standard entity fields. 23 | * 24 | * @var array 25 | */ 26 | protected $standardFields = [ 27 | 'ID', 28 | 'IS_ONLINE', 29 | 'LAST_ACTIVITY_DATE', 30 | 'AUTO_TIME_ZONE', 31 | 'TIME_ZONE', 32 | 'CONFIRM_CODE', 33 | 'STORED_HASH', 34 | 'EXTERNAL_AUTH_ID', 35 | 'LOGIN_ATTEMPTS', 36 | 'CHECKWORD', 37 | 'CHECKWORD_TIME', 38 | 'DATE_REGISTER', 39 | 'TIMESTAMP_X', 40 | 'LAST_LOGIN', 41 | 'ACTIVE', 42 | 'BLOCKED', 43 | 'TITLE', 44 | 'NAME', 45 | 'LAST_NAME', 46 | 'SECOND_NAME', 47 | 'EMAIL', 48 | 'LOGIN', 49 | 'PHONE_NUMBER', 50 | 'PASSWORD', 51 | 'XML_ID', 52 | 'LID', 53 | 'LANGUAGE_ID', 54 | 'PERSONAL_PROFESSION', 55 | 'PERSONAL_WWW', 56 | 'PERSONAL_ICQ', 57 | 'PERSONAL_GENDER', 58 | 'PERSONAL_BIRTHDAY', 59 | 'PERSONAL_PHOTO', 60 | 'PERSONAL_PHONE', 61 | 'PERSONAL_FAX', 62 | 'PERSONAL_MOBILE', 63 | 'PERSONAL_PAGER', 64 | 'PERSONAL_COUNTRY', 65 | 'PERSONAL_STATE', 66 | 'PERSONAL_CITY', 67 | 'PERSONAL_ZIP', 68 | 'PERSONAL_STREET', 69 | 'PERSONAL_MAILBOX', 70 | 'PERSONAL_NOTES', 71 | 'WORK_COMPANY', 72 | 'WORK_WWW', 73 | 'WORK_DEPARTMENT', 74 | 'WORK_POSITION', 75 | 'WORK_PROFILE', 76 | 'WORK_LOGO', 77 | 'WORK_PHONE', 78 | 'WORK_FAX', 79 | 'WORK_PAGER', 80 | 'WORK_COUNTRY', 81 | 'WORK_STATE', 82 | 'WORK_CITY', 83 | 'WORK_ZIP', 84 | 'WORK_STREET', 85 | 'WORK_MAILBOX', 86 | 'WORK_NOTES', 87 | 'ADMIN_NOTES', 88 | ]; 89 | 90 | /** 91 | * Get the collection of users according to the current query. 92 | * 93 | * @return Collection 94 | */ 95 | protected function loadModels() 96 | { 97 | $queryType = 'UserQuery::getList'; 98 | $sort = $this->sort; 99 | $filter = $this->normalizeFilter(); 100 | $params = [ 101 | 'SELECT' => $this->propsMustBeSelected() ? ['UF_*'] : ($this->normalizeUfSelect() ?: false), 102 | 'NAV_PARAMS' => $this->navigation, 103 | 'FIELDS' => $this->normalizeSelect(), 104 | ]; 105 | $selectGroups = $this->groupsMustBeSelected(); 106 | $keyBy = $this->keyBy; 107 | 108 | $callback = function () use ($sort, $filter, $params, $selectGroups) { 109 | $users = []; 110 | $rsUsers = $this->bxObject->getList($sort, $sortOrder = false, $filter, $params); 111 | while ($arUser = $this->performFetchUsingSelectedMethod($rsUsers)) { 112 | if ($selectGroups) { 113 | $arUser['GROUP_ID'] = $this->bxObject->getUserGroup($arUser['ID']); 114 | } 115 | 116 | $this->addItemToResultsUsingKeyBy($users, new $this->modelName($arUser['ID'], $arUser)); 117 | } 118 | 119 | return new Collection($users); 120 | }; 121 | 122 | return $this->handleCacheIfNeeded(compact('queryType', 'sort', 'filter', 'params', 'selectGroups', 'keyBy'), $callback); 123 | } 124 | 125 | /** 126 | * Get the first user with a given login. 127 | * 128 | * @param string $login 129 | * 130 | * @return UserModel 131 | */ 132 | public function getByLogin($login) 133 | { 134 | $this->filter['LOGIN_EQUAL_EXACT'] = $login; 135 | 136 | return $this->first(); 137 | } 138 | 139 | /** 140 | * Get the first user with a given email. 141 | * 142 | * @param string $email 143 | * 144 | * @return UserModel 145 | */ 146 | public function getByEmail($email) 147 | { 148 | $this->filter['EMAIL'] = $email; 149 | 150 | return $this->first(); 151 | } 152 | 153 | /** 154 | * Get count of users according the current query. 155 | * 156 | * @return int 157 | */ 158 | public function count() 159 | { 160 | if ($this->queryShouldBeStopped) { 161 | return 0; 162 | } 163 | 164 | $queryType = 'UserQuery::count'; 165 | $filter = $this->normalizeFilter(); 166 | $callback = function () use ($filter) { 167 | return (int) $this->bxObject->getList($order = 'ID', $by = 'ASC', $filter, [ 168 | 'NAV_PARAMS' => [ 169 | 'nTopCount' => 0, 170 | ], 171 | ])->NavRecordCount; 172 | }; 173 | 174 | return $this->handleCacheIfNeeded(compact('queryType', 'filter'), $callback); 175 | } 176 | 177 | /** 178 | * Determine if groups must be selected. 179 | * 180 | * @return bool 181 | */ 182 | protected function groupsMustBeSelected() 183 | { 184 | return in_array('GROUPS', $this->select) || in_array('GROUP_ID', $this->select) || in_array('GROUPS_ID', $this->select); 185 | } 186 | 187 | /** 188 | * Normalize filter before sending it to getList. 189 | * This prevents some inconsistency. 190 | * 191 | * @return array 192 | */ 193 | protected function normalizeFilter() 194 | { 195 | $this->substituteField($this->filter, 'GROUPS', 'GROUPS_ID'); 196 | $this->substituteField($this->filter, 'GROUP_ID', 'GROUPS_ID'); 197 | 198 | return $this->filter; 199 | } 200 | 201 | /** 202 | * Normalize select before sending it to getList. 203 | * This prevents some inconsistency. 204 | * 205 | * @return array 206 | */ 207 | protected function normalizeSelect() 208 | { 209 | if ($this->fieldsMustBeSelected()) { 210 | $this->select = array_merge($this->standardFields, $this->select); 211 | } 212 | 213 | $this->select[] = 'ID'; 214 | 215 | return $this->clearSelectArray(); 216 | } 217 | 218 | /** 219 | * Normalize select UF before sending it to getList. 220 | * 221 | * @return array 222 | */ 223 | protected function normalizeUfSelect() 224 | { 225 | return preg_grep('/^(UF_+)/', $this->select); 226 | } 227 | 228 | protected function prepareMultiFilter(&$key, &$value) 229 | { 230 | $value = join(' | ', $value); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | addEventHandler('main', 'OnAfterEpilog', [IlluminateQueryDebugger::class, 'onAfterEpilogHandler']); 46 | } 47 | 48 | static::addEventListenersForHelpersHighloadblockTables($capsule); 49 | } 50 | 51 | /** 52 | * Bootstrap illuminate/pagination 53 | */ 54 | protected static function bootstrapIlluminatePagination() 55 | { 56 | if (class_exists(BladeProvider::class)) { 57 | Paginator::viewFactoryResolver(function () { 58 | return BladeProvider::getViewFactory(); 59 | }); 60 | } 61 | 62 | Paginator::$defaultView = 'pagination.default'; 63 | Paginator::$defaultSimpleView = 'pagination.simple-default'; 64 | 65 | Paginator::currentPathResolver(function () { 66 | return $GLOBALS['APPLICATION']->getCurPage(); 67 | }); 68 | 69 | Paginator::currentPageResolver(function ($pageName = 'page') { 70 | $page = $_GET[$pageName]; 71 | 72 | if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int)$page >= 1) { 73 | return $page; 74 | } 75 | 76 | return 1; 77 | }); 78 | } 79 | 80 | /** 81 | * Bootstrap illuminate/database 82 | * @return Capsule 83 | */ 84 | protected static function bootstrapIlluminateDatabase() 85 | { 86 | $capsule = new Capsule(self::instantiateServiceContainer()); 87 | 88 | if ($dbConfig = Configuration::getInstance()->get('bitrix-models.illuminate-database')) { 89 | foreach ($dbConfig['connections'] as $name => $connection) { 90 | $capsule->addConnection($connection, $name); 91 | } 92 | 93 | $capsule->getDatabaseManager()->setDefaultConnection((isset($dbConfig['default'])) ? $dbConfig['default'] : 'default'); 94 | } else { 95 | $config = self::getBitrixDbConfig(); 96 | 97 | $capsule->addConnection([ 98 | 'driver' => 'mysql', 99 | 'host' => $config['host'], 100 | 'database' => $config['database'], 101 | 'username' => $config['login'], 102 | 'password' => $config['password'], 103 | 'charset' => 'utf8', 104 | 'collation' => 'utf8_unicode_ci', 105 | 'prefix' => '', 106 | 'strict' => false, 107 | ]); 108 | } 109 | 110 | if (class_exists(Dispatcher::class)) { 111 | $capsule->setEventDispatcher(new Dispatcher()); 112 | } 113 | 114 | $capsule->setAsGlobal(); 115 | $capsule->bootEloquent(); 116 | 117 | static::$illuminateDatabaseIsUsed = true; 118 | 119 | return $capsule; 120 | } 121 | 122 | /** 123 | * Instantiate service container if it's not instantiated yet. 124 | */ 125 | protected static function instantiateServiceContainer() 126 | { 127 | $container = Container::getInstance(); 128 | 129 | if (!$container) { 130 | $container = new Container(); 131 | Container::setInstance($container); 132 | } 133 | 134 | return $container; 135 | } 136 | 137 | /** 138 | * Get bitrix database configuration array. 139 | * 140 | * @return array 141 | */ 142 | protected static function getBitrixDbConfig() 143 | { 144 | $config = Configuration::getInstance(); 145 | $connections = $config->get('connections'); 146 | 147 | return $connections['default']; 148 | } 149 | 150 | /** 151 | * Для множественных полей Highload блоков битрикс использует вспомогательные таблицы. 152 | * Данный метод вешает обработчики на eloquent события добавления и обновления записей которые будут актуализировать и эти таблицы. 153 | * 154 | * @param Capsule $capsule 155 | */ 156 | private static function addEventListenersForHelpersHighloadblockTables(Capsule $capsule) 157 | { 158 | $dispatcher = $capsule->getEventDispatcher(); 159 | if (!$dispatcher) { 160 | return; 161 | } 162 | 163 | $dispatcher->listen(['eloquent.deleted: *'], function ($event, $payload) { 164 | /** @var EloquentModel $model */ 165 | $model = $payload[0]; 166 | if (empty($model->multipleHighloadBlockFields)) { 167 | return; 168 | } 169 | 170 | $modelTable = $model->getTable(); 171 | foreach ($model->multipleHighloadBlockFields as $multipleHighloadBlockField) { 172 | if (!empty($model['ID'])) { 173 | $tableName = $modelTable . '_' . strtolower($multipleHighloadBlockField); 174 | DB::table($tableName)->where('ID', $model['ID'])->delete(); 175 | } 176 | } 177 | }); 178 | 179 | $dispatcher->listen(['eloquent.updated: *', 'eloquent.created: *'], function ($event, $payload) { 180 | /** @var EloquentModel $model */ 181 | $model = $payload[0]; 182 | if (empty($model->multipleHighloadBlockFields)) { 183 | return; 184 | } 185 | 186 | $dirty = $model->getDirty(); 187 | $modelTable = $model->getTable(); 188 | foreach ($model->multipleHighloadBlockFields as $multipleHighloadBlockField) { 189 | if (isset($dirty[$multipleHighloadBlockField]) && !empty($model['ID'])) { 190 | $tableName = $modelTable . '_' . strtolower($multipleHighloadBlockField); 191 | 192 | if (substr($event, 0, 16) === 'eloquent.updated') { 193 | DB::table($tableName)->where('ID', $model['ID'])->delete(); 194 | } 195 | 196 | $unserializedValues = unserialize($dirty[$multipleHighloadBlockField]); 197 | if (!$unserializedValues) { 198 | continue; 199 | } 200 | 201 | $newRows = []; 202 | foreach ($unserializedValues as $unserializedValue) { 203 | $newRows[] = [ 204 | 'ID' => $model['ID'], 205 | 'VALUE' => $unserializedValue, 206 | ]; 207 | } 208 | 209 | if ($newRows) { 210 | DB::table($tableName)->insert($newRows); 211 | } 212 | } 213 | } 214 | }); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /tests/D7ModelTest.php: -------------------------------------------------------------------------------- 1 | assertSame(1, $element->id); 16 | 17 | $fields = [ 18 | 'UF_EMAIL' => 'John', 19 | 'UF_IMAGE_ID' => '1', 20 | ]; 21 | $element = new TestD7Element(1, $fields); 22 | $this->assertSame(1, $element->id); 23 | $this->assertSame($fields, $element->fields); 24 | } 25 | 26 | public function testMultipleInitialization() 27 | { 28 | // 1 29 | $element = new TestD7Element(1); 30 | $this->assertSame(1, $element->id); 31 | 32 | $fields = [ 33 | 'UF_EMAIL' => 'John', 34 | 'UF_IMAGE_ID' => '1', 35 | ]; 36 | $element = new TestD7Element(1, $fields); 37 | $this->assertSame(1, $element->id); 38 | $this->assertSame($fields, $element->fields); 39 | 40 | // 2 41 | $element2 = new TestD7Element2(1); 42 | $this->assertSame(1, $element2->id); 43 | 44 | $fields = [ 45 | 'UF_EMAIL' => 'John', 46 | 'UF_IMAGE_ID' => '1', 47 | ]; 48 | $element2 = new TestD7Element2(1, $fields); 49 | $this->assertSame(1, $element2->id); 50 | $this->assertSame($fields, $element2->fields); 51 | 52 | // dd([TestD7Element::cachedTableClass(), TestD7Element2::cachedTableClass()]); 53 | $this->assertTrue(TestD7Element::cachedTableClass() !== TestD7Element2::cachedTableClass()); 54 | $this->assertTrue(TestD7Element::instantiateAdapter() !== TestD7Element2::instantiateAdapter()); 55 | } 56 | 57 | public function testAdd() 58 | { 59 | $resultObject = new TestD7ResultObject(); 60 | $adapter = m::mock('adapter'); 61 | $adapter->shouldReceive('add')->once()->with(['UF_NAME' => 'Jane', 'UF_AGE' => '18'])->andReturn($resultObject); 62 | 63 | TestD7Element::setAdapter($adapter); 64 | $element = TestD7Element::create(['UF_NAME' => 'Jane', 'UF_AGE' => '18']); 65 | $this->assertEquals($element->id, 1); 66 | $this->assertEquals($element->fields, ['UF_NAME' => 'Jane', 'UF_AGE' => '18', 'ID' => '1']); 67 | } 68 | 69 | public function testUpdate() 70 | { 71 | $resultObject = new TestD7ResultObject(); 72 | $adapter = m::mock('adapter'); 73 | $adapter->shouldReceive('update')->once()->with(1, ['UF_NAME' => 'Jane'])->andReturn($resultObject); 74 | 75 | $element = new TestD7Element(1); 76 | TestD7Element::setAdapter($adapter); 77 | 78 | 79 | $this->assertTrue($element->update(['UF_NAME' => 'Jane'])); 80 | } 81 | 82 | public function testDelete() 83 | { 84 | // normal 85 | $resultObject = new TestD7ResultObject(); 86 | $adapter = m::mock('adapter'); 87 | $adapter->shouldReceive('delete')->once()->with(1)->andReturn($resultObject); 88 | 89 | $element = m::mock('Arrilot\Tests\BitrixModels\Stubs\TestD7Element[onAfterDelete, onBeforeDelete]', [1]) 90 | ->shouldAllowMockingProtectedMethods(); 91 | $element::setAdapter($adapter); 92 | $element->shouldReceive('onBeforeDelete')->once()->andReturn(null); 93 | $element->shouldReceive('onAfterDelete')->once()->with(true); 94 | 95 | $this->assertTrue($element->delete()); 96 | 97 | // cancelled 98 | $element = m::mock('Arrilot\Tests\BitrixModels\Stubs\TestD7Element[onAfterDelete, onBeforeDelete]', [1]) 99 | ->shouldAllowMockingProtectedMethods(); 100 | $element->shouldReceive('onBeforeDelete')->once()->andReturn(false); 101 | $element->shouldReceive('onAfterDelete')->never(); 102 | $this->assertFalse($element->delete()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/D7QueryTest.php: -------------------------------------------------------------------------------- 1 | setAdapter($adapter); 26 | } 27 | 28 | public function testCount() 29 | { 30 | $adapter = m::mock('D7Adapter'); 31 | $adapter->shouldReceive('getClassName')->once()->andReturn('TestD7ClassName'); 32 | $adapter->shouldReceive('getCount')->once()->andReturn(6); 33 | 34 | $query = $this->createQuery($adapter); 35 | $count = $query->count(); 36 | $this->assertSame(6, $count); 37 | 38 | $adapter = m::mock('D7Adapter'); 39 | $adapter->shouldReceive('getClassName')->once()->andReturn('TestD7ClassName'); 40 | $adapter->shouldReceive('getCount')->with(['>ID' => 5])->once()->andReturn(3); 41 | 42 | $query = $this->createQuery($adapter); 43 | $count = $query->filter(['>ID' => 5])->count(); 44 | $this->assertSame(3, $count); 45 | } 46 | 47 | public function testGetList() 48 | { 49 | $adapter = m::mock('D7Adapter'); 50 | $params = [ 51 | 'select' => ['ID', 'UF_NAME'], 52 | 'filter' => ['UF_NAME' => 'John'], 53 | 'group' => [], 54 | 'order' => ['ID' => 'ASC'], 55 | 'limit' => null, 56 | 'offset' => null, 57 | 'runtime' => [], 58 | ]; 59 | $adapter->shouldReceive('getClassName')->once()->andReturn('TestD7ClassName'); 60 | $adapter->shouldReceive('getList')->with($params)->once()->andReturn(m::self()); 61 | $adapter->shouldReceive('fetch')->andReturn(['ID' => 1, 'UF_NAME' => 'John Doe'], ['ID' => 2, 'UF_NAME' => 'John Doe 2'], false); 62 | 63 | $query = $this->createQuery($adapter); 64 | $items = $query->sort(['ID' => 'ASC'])->filter(['UF_NAME' => 'John'])->select(['ID', 'UF_NAME'])->getList(); 65 | 66 | $expected = [ 67 | 1 => ['ID' => 1, 'UF_NAME' => 'John Doe'], 68 | 2 => ['ID' => 2, 'UF_NAME' => 'John Doe 2'], 69 | ]; 70 | foreach ($items as $k => $item) { 71 | $this->assertSame($expected[$k], $item->toArray()); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/ElementQueryTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('GetList')->with([], ['IBLOCK_ID' => 1], [])->once()->andReturn(6); 35 | 36 | $query = $this->createQuery($bxObject); 37 | $count = $query->count(); 38 | 39 | $this->assertSame(6, $count); 40 | 41 | $bxObject = m::mock('obj'); 42 | TestElement::$bxObject = $bxObject; 43 | $bxObject->shouldReceive('GetList')->with([], ['ACTIVE' => 'Y', 'IBLOCK_ID' => 1], [])->once()->andReturn(3); 44 | 45 | $query = $this->createQuery($bxObject); 46 | $count = $query->filter(['ACTIVE' => 'Y'])->count(); 47 | 48 | $this->assertSame(3, $count); 49 | } 50 | 51 | public function testGetListWithSelectAndFilter() 52 | { 53 | $bxObject = m::mock('obj'); 54 | TestElement::$bxObject = $bxObject; 55 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', '!CODE' => false, 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 56 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 57 | 58 | $query = $this->createQuery($bxObject); 59 | $items = $query->filter(['ACTIVE' => 'N'])->addFilter(['!CODE' => false])->select('ID', 'NAME')->getList(); 60 | 61 | $expected = [ 62 | 1 => ['ID' => 1, 'NAME' => 'foo'], 63 | 2 => ['ID' => 2, 'NAME' => 'bar'], 64 | ]; 65 | foreach ($items as $k => $item) { 66 | $this->assertSame($expected[$k], $item->fields); 67 | } 68 | } 69 | 70 | public function testGetListWithKeyBy() 71 | { 72 | $bxObject = m::mock('obj'); 73 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 74 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 75 | 76 | $query = $this->createQuery($bxObject); 77 | $items = $query->filter(['ACTIVE' => 'N'])->select('ID', 'NAME')->getList(); 78 | 79 | $expected = [ 80 | 1 => ['ID' => 1, 'NAME' => 'foo', 'ACCESSOR_THREE' => [], 'PROPERTY_LANG_ACCESSOR_ONE' => null], 81 | 2 => ['ID' => 2, 'NAME' => 'bar', 'ACCESSOR_THREE' => [], 'PROPERTY_LANG_ACCESSOR_ONE' => null], 82 | ]; 83 | 84 | $this->assertSame($expected, $items->toArray()); 85 | $this->assertSame(json_encode($expected), $items->toJson()); 86 | 87 | $bxObject = m::mock('obj'); 88 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 89 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 90 | 91 | $query = $this->createQuery($bxObject); 92 | $items = $query->filter(['ACTIVE' => 'N'])->keyBy('NAME')->select(['ID', 'NAME'])->getList(); 93 | 94 | $expected = [ 95 | 'foo' => ['ID' => 1, 'NAME' => 'foo'], 96 | 'bar' => ['ID' => 2, 'NAME' => 'bar'], 97 | ]; 98 | foreach ($items as $k => $item) { 99 | $this->assertSame($expected[$k], $item->fields); 100 | } 101 | } 102 | 103 | public function testGetListWithKeyByAndMissingKey() 104 | { 105 | $this->setExpectedException('LogicException'); 106 | 107 | $bxObject = m::mock('obj'); 108 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 109 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 110 | 111 | $query = $this->createQuery($bxObject); 112 | $items = $query->filter(['ACTIVE' => 'N'])->keyBy('GUID')->select(['ID', 'NAME'])->getList(); 113 | 114 | $expected = [ 115 | 'foo' => ['ID' => 1, 'NAME' => 'foo'], 116 | 'bar' => ['ID' => 2, 'NAME' => 'bar'], 117 | ]; 118 | foreach ($items as $k => $item) { 119 | $this->assertSame($expected[$k], $item->fields); 120 | } 121 | } 122 | 123 | public function testGetListGroupsItemsByKeyBy() 124 | { 125 | $bxObject = m::mock('obj'); 126 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', 'IBLOCK_ID' => 1], false, false, ['ID', 'PROPERTY_FOO', 'IBLOCK_ID'])->once()->andReturn(m::self()); 127 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'PROPERTY_FOO_VALUE' => 'foo'], ['ID' => 2, 'PROPERTY_FOO_VALUE' => 'bar'], ['ID' => 2, 'PROPERTY_FOO_VALUE' => 'bar2'], ['ID' => 2, 'PROPERTY_FOO_VALUE' => 'bar3'], false); 128 | 129 | $query = $this->createQuery($bxObject); 130 | $items = $query->filter(['ACTIVE' => 'N'])->select(['ID', 'PROPERTY_FOO'])->getList(); 131 | 132 | $expected = [ 133 | 1 => ['ID' => 1, 'PROPERTY_FOO_VALUE' => 'foo'], 134 | 2 => ['ID' => 2, 'PROPERTY_FOO_VALUE' => ['bar', 'bar2', 'bar3'], '_were_multiplied' => ['PROPERTY_FOO_VALUE' => true]], 135 | ]; 136 | foreach ($items as $k => $item) { 137 | $this->assertSame($expected[$k], $item->fields); 138 | } 139 | } 140 | 141 | public function testResetFilter() 142 | { 143 | $bxObject = m::mock('obj'); 144 | TestElement::$bxObject = $bxObject; 145 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 146 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 147 | 148 | $query = $this->createQuery($bxObject); 149 | $items = $query->filter(['NAME' => 'John'])->resetFilter()->select('ID', 'NAME')->getList(); 150 | 151 | $expected = [ 152 | 1 => ['ID' => 1, 'NAME' => 'foo'], 153 | 2 => ['ID' => 2, 'NAME' => 'bar'], 154 | ]; 155 | foreach ($items as $k => $item) { 156 | $this->assertSame($expected[$k], $item->fields); 157 | } 158 | } 159 | 160 | public function testScopeActive() 161 | { 162 | $bxObject = m::mock('obj'); 163 | TestElement::$bxObject = $bxObject; 164 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['NAME' => 'John', 'ACTIVE' => 'Y', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 165 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 166 | 167 | $query = $this->createQuery($bxObject); 168 | $items = $query->active()->filter(['NAME' => 'John'])->select('ID', 'NAME')->getList(); 169 | 170 | $expected = [ 171 | 1 => ['ID' => 1, 'NAME' => 'foo'], 172 | 2 => ['ID' => 2, 'NAME' => 'bar'], 173 | ]; 174 | foreach ($items as $k => $item) { 175 | $this->assertSame($expected[$k], $item->fields); 176 | } 177 | } 178 | 179 | public function testFromSection() 180 | { 181 | $bxObject = m::mock('obj'); 182 | TestElement::$bxObject = $bxObject; 183 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['SECTION_ID' => 15, 'SECTION_CODE' => 'articles', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 184 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 185 | 186 | $query = $this->createQuery($bxObject); 187 | $items = $query 188 | ->fromSectionWithId(15) 189 | ->fromSectionWithCode('articles') 190 | ->select('ID', 'NAME') 191 | ->getList(); 192 | 193 | $expected = [ 194 | 1 => ['ID' => 1, 'NAME' => 'foo'], 195 | 2 => ['ID' => 2, 'NAME' => 'bar'], 196 | ]; 197 | foreach ($items as $k => $item) { 198 | $this->assertSame($expected[$k], $item->fields); 199 | } 200 | } 201 | 202 | public function testGetById() 203 | { 204 | $bxObject = m::mock('obj'); 205 | $query = m::mock('Arrilot\BitrixModels\Queries\ElementQuery[getList]', [$bxObject, 'Arrilot\Tests\BitrixModels\Stubs\TestElement', 1]); 206 | $query->shouldReceive('getList')->once()->andReturn(new Collection([ 207 | 1 => [ 208 | 'ID' => 1, 209 | 'NAME' => 2, 210 | ], 211 | ])); 212 | 213 | $this->assertSame(['ID' => 1, 'NAME' => 2], $query->getById(1)); 214 | $this->assertSame(false, $query->getById(0)); 215 | } 216 | 217 | // public function testGetListWithFetchUsing() 218 | // { 219 | // $bxObject = m::mock('obj'); 220 | // $bxObject->shouldReceive('getList') 221 | // ->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'PROPERTY_GUID', 'IBLOCK_ID']) 222 | // ->once() 223 | // ->andReturn(m::self()); 224 | // $bxObject->shouldReceive('Fetch')->andReturn( 225 | // ['ID' => 1, 'NAME' => 'foo', 'PROPERTY_GUID_VALUE' => 'foo'], 226 | // ['ID' => 2, 'NAME' => 'bar', 'PROPERTY_GUID_VALUE' => ''], 227 | // false 228 | // ); 229 | // 230 | // TestElement::$bxObject = $bxObject; 231 | // $query = $this->createQuery($bxObject); 232 | // $items = $query->filter(['ACTIVE' => 'N'])->select('ID', 'NAME', 'PROPERTY_GUID')->fetchUsing('Fetch')->getList(); 233 | // 234 | // $expected = [ 235 | // 1 => ['ID' => 1, 'NAME' => 'foo', 'PROPERTY_GUID_VALUE' => 'foo'], 236 | // 2 => ['ID' => 2, 'NAME' => 'bar', 'PROPERTY_GUID_VALUE' => ''], 237 | // ]; 238 | // foreach ($items as $k => $item) { 239 | // $this->assertSame($expected[$k], $item->fields); 240 | // } 241 | // } 242 | // 243 | // public function testGetListWithFetchUsingAndNoProps() 244 | // { 245 | // $bxObject = m::mock('obj'); 246 | // $bxObject->shouldReceive('getList')->with(['SORT' => 'ASC'], ['ACTIVE' => 'N', 'IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 247 | // $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 248 | // 249 | // TestElement::$bxObject = $bxObject; 250 | // $query = $this->createQuery($bxObject); 251 | // $items = $query->filter(['ACTIVE' => 'N'])->select('ID', 'NAME')->fetchUsing('Fetch')->getList(); 252 | // 253 | // $expected = [ 254 | // 1 => ['ID' => 1, 'NAME' => 'foo'], 255 | // 2 => ['ID' => 2, 'NAME' => 'bar'], 256 | // ]; 257 | // foreach ($items as $k => $item) { 258 | // $this->assertSame($expected[$k], $item->fields); 259 | // } 260 | // } 261 | 262 | public function testLimitAndPage() 263 | { 264 | $bxObject = m::mock('obj'); 265 | TestElement::$bxObject = $bxObject; 266 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['NAME' => 'John', 'IBLOCK_ID' => 1], false, ['iNumPage' => 3, 'nPageSize' => 2], ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 267 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 268 | 269 | $query = $this->createQuery($bxObject); 270 | $items = $query->filter(['NAME' => 'John'])->page(3)->limit(2)->select('ID', 'NAME')->getList(); 271 | 272 | $expected = [ 273 | 1 => ['ID' => 1, 'NAME' => 'foo'], 274 | 2 => ['ID' => 2, 'NAME' => 'bar'], 275 | ]; 276 | foreach ($items as $k => $item) { 277 | $this->assertSame($expected[$k], $item->fields); 278 | } 279 | } 280 | 281 | public function testSort() 282 | { 283 | $bxObject = m::mock('obj'); 284 | TestElement::$bxObject = $bxObject; 285 | $bxObject->shouldReceive('GetList')->with(['NAME' => 'DESC'], ['IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 286 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 287 | 288 | $query = $this->createQuery($bxObject); 289 | $query->sort(['NAME' => 'DESC']) 290 | ->select('ID', 'NAME') 291 | ->getList(); 292 | 293 | $bxObject = m::mock('obj'); 294 | TestElement::$bxObject = $bxObject; 295 | $bxObject->shouldReceive('GetList')->with(['NAME' => 'ASC'], ['IBLOCK_ID' => 1], false, false, ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 296 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 297 | 298 | $query = $this->createQuery($bxObject); 299 | $query->sort('NAME') 300 | ->select('ID', 'NAME') 301 | ->getList(); 302 | } 303 | 304 | public function testFirst() 305 | { 306 | $bxObject = m::mock('obj'); 307 | TestElement::$bxObject = $bxObject; 308 | $bxObject->shouldReceive('GetList')->with(['SORT' => 'ASC'], ['NAME' => 'John', 'IBLOCK_ID' => 1], false, ['nPageSize' => 1], ['ID', 'NAME', 'IBLOCK_ID'])->once()->andReturn(m::self()); 309 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], false); 310 | 311 | $query = $this->createQuery($bxObject); 312 | $item = $query->filter(['NAME' => 'John'])->select('ID', 'NAME')->first(); 313 | 314 | $this->assertSame(['ID' => 1, 'NAME' => 'foo'], $item->fields); 315 | } 316 | 317 | public function testStopAction() 318 | { 319 | $bxObject = m::mock('obj'); 320 | TestElement::$bxObject = $bxObject; 321 | 322 | $query = $this->createQuery($bxObject); 323 | $items = $query->filter(['NAME' => 'John'])->stopQuery()->getList(); 324 | $this->assertSame((new Collection())->all(), $items->all()); 325 | 326 | $query = $this->createQuery($bxObject); 327 | $item = $query->filter(['NAME' => 'John'])->stopQuery()->getById(1); 328 | $this->assertSame(false, $item); 329 | 330 | $query = $this->createQuery($bxObject); 331 | $count = $query->filter(['NAME' => 'John'])->stopQuery()->count(); 332 | $this->assertSame(0, $count); 333 | } 334 | 335 | public function testStopActionFromScope() 336 | { 337 | $bxObject = m::mock('obj'); 338 | TestElement::$bxObject = $bxObject; 339 | 340 | $query = $this->createQuery($bxObject); 341 | $items = $query->filter(['NAME' => 'John'])->stopActionScope()->getList(); 342 | $this->assertSame((new Collection())->all(), $items->all()); 343 | 344 | $query = $this->createQuery($bxObject); 345 | $item = $query->filter(['NAME' => 'John'])->stopActionScope()->getById(1); 346 | $this->assertSame(false, $item); 347 | 348 | $query = $this->createQuery($bxObject); 349 | $count = $query->filter(['NAME' => 'John'])->stopActionScope()->count(); 350 | $this->assertSame(0, $count); 351 | } 352 | 353 | public function testPaginate() 354 | { 355 | if (!class_exists('Illuminate\Pagination\LengthAwarePaginator')) { 356 | $this->markTestSkipped(); 357 | } 358 | $query = m::mock('Arrilot\BitrixModels\Queries\ElementQuery[getList, count]', [null, 'Arrilot\Tests\BitrixModels\Stubs\TestElement']) 359 | ->shouldAllowMockingProtectedMethods(); 360 | 361 | $query->shouldReceive('count')->once()->andReturn(100); 362 | $query->shouldReceive('getList')->once()->andReturn(collect(range(1, 15))); 363 | 364 | $items = $query->paginate(); 365 | 366 | $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $items); 367 | } 368 | 369 | public function testSimplePaginate() 370 | { 371 | if (!class_exists('Illuminate\Pagination\Paginator')) { 372 | $this->markTestSkipped(); 373 | } 374 | 375 | $query = m::mock('Arrilot\BitrixModels\Queries\ElementQuery[getList, count]', [null, 'Arrilot\Tests\BitrixModels\Stubs\TestElement']) 376 | ->shouldAllowMockingProtectedMethods(); 377 | 378 | $query->shouldReceive('count')->never(); 379 | $query->shouldReceive('getList')->once()->andReturn(collect(range(1, 15))); 380 | 381 | $items = $query->simplePaginate(); 382 | 383 | $this->assertInstanceOf('Illuminate\Pagination\Paginator', $items); 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /tests/RelationTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('GetProperties')->withAnyArgs()->andReturn(m::self()); 17 | $cIblockObject->shouldReceive('Fetch')->times(3)->andReturn( 18 | [ 19 | 'ID' => '1', 20 | 'IBLOCK_ID' => TestElement::IBLOCK_ID, 21 | 'CODE' => 'ELEMENT', 22 | ], 23 | false, 24 | false 25 | ); 26 | ElementQuery::$cIblockObject = $cIblockObject; 27 | 28 | $bxObject = m::mock('obj'); 29 | $bxObject->shouldReceive('GetList')->withAnyArgs()->once()->andReturn(m::self()); 30 | $bxObject->shouldReceive('Fetch')->times(2)->andReturn( 31 | [ 32 | 'ID' => 1, 33 | 'NAME' => 'Название', 34 | 'PROPERTY_ELEMENT_VALUE' => ['1', '2'], 35 | 'PROPERTY_ELEMENT_DESCRIPTION' => ['', ''], 36 | 'PROPERTY_ELEMENT_VALUE_ID' => ['element_prop_id_1', 'element_prop_id_2'], 37 | ], 38 | false 39 | ); 40 | TestElement::$bxObject = $bxObject; 41 | 42 | $product = TestElement::getById(1); 43 | 44 | $bxObject = m::mock('obj'); 45 | $bxObject->shouldReceive('GetList')->with(m::any(), ['IBLOCK_ID' => TestElement2::IBLOCK_ID, 'ID' => [1, 2]], m::any(), false, m::any())->once()->andReturn(m::self()); 46 | $bxObject->shouldReceive('Fetch')->times(3)->andReturn( 47 | [ 48 | 'ID' => 1, 49 | 'NAME' => 'Название', 50 | ], 51 | [ 52 | 'ID' => 2, 53 | 'NAME' => 'Название 2', 54 | ], 55 | false 56 | ); 57 | TestElement::$bxObject = $bxObject; 58 | 59 | 60 | $this->assertInstanceOf(Collection::class, $product->elements); 61 | $this->assertCount(2, $product->elements); 62 | $this->assertEquals(['ID' => 1, 'NAME' => 'Название'], $product->elements[1]->fields); 63 | $this->assertEquals(['ID' => 2, 'NAME' => 'Название 2'], $product->elements[2]->fields); 64 | } 65 | 66 | public function testWith() 67 | { 68 | $cIblockObject = m::mock('cIblockObject'); 69 | $cIblockObject->shouldReceive('GetProperties')->withAnyArgs()->andReturn(m::self()); 70 | $cIblockObject->shouldReceive('Fetch')->times(3)->andReturn( 71 | false, 72 | [ 73 | 'ID' => '1', 74 | 'IBLOCK_ID' => TestElement::IBLOCK_ID, 75 | 'CODE' => 'ELEMENT', 76 | ], 77 | false 78 | ); 79 | ElementQuery::$cIblockObject = $cIblockObject; 80 | 81 | $bxObject = m::mock('obj'); 82 | $bxObject->shouldReceive('GetList')->withAnyArgs()->twice()->andReturn(m::self()); 83 | $brandField = [ 84 | 'ID' => 1, 85 | 'NAME' => 'Название', 86 | 'PROPERTY_ELEMENT_VALUE' => '1', 87 | 'PROPERTY_ELEMENT_DESCRIPTION' => '', 88 | 'PROPERTY_ELEMENT_VALUE_ID' => 'element_prop_id', 89 | ]; 90 | 91 | $bxObject->shouldReceive('Fetch')->times(4)->andReturn( 92 | [ 93 | 'ID' => 1, 94 | 'NAME' => 'Название', 95 | ], 96 | false, 97 | $brandField, 98 | false 99 | ); 100 | TestElement::$bxObject = $bxObject; 101 | 102 | $product = TestElement::query()->with('element')->getById(1); 103 | 104 | // Проверяем что все запросы были выполнены до текущего момента 105 | $cIblockObject->mockery_verify(); 106 | $bxObject->mockery_verify(); 107 | 108 | $this->assertEquals($brandField, $product->element->fields); 109 | } 110 | 111 | public function testOne() 112 | { 113 | $cIblockObject = m::mock('cIblockObject'); 114 | $cIblockObject->shouldReceive('GetProperties')->withAnyArgs()->andReturn(m::self()); 115 | $cIblockObject->shouldReceive('Fetch')->times(3)->andReturn( 116 | false, 117 | [ 118 | 'ID' => '1', 119 | 'IBLOCK_ID' => TestElement::IBLOCK_ID, 120 | 'CODE' => 'ELEMENT', 121 | ], 122 | false 123 | ); 124 | ElementQuery::$cIblockObject = $cIblockObject; 125 | 126 | $bxObject = m::mock('obj'); 127 | $bxObject->shouldReceive('GetList')->withAnyArgs()->once()->andReturn(m::self()); 128 | $bxObject->shouldReceive('Fetch')->times(2)->andReturn( 129 | [ 130 | 'ID' => 1, 131 | 'NAME' => 'Название', 132 | ], 133 | false 134 | ); 135 | TestElement::$bxObject = $bxObject; 136 | 137 | $product = TestElement::getById(1); 138 | 139 | 140 | $bxObject = m::mock('obj'); 141 | $bxObject->shouldReceive('GetList')->with(m::any(), ['IBLOCK_ID' => TestElement2::IBLOCK_ID, 'PROPERTY_ELEMENT' => 1], m::any(), ['nPageSize' => 1], m::any())->once()->andReturn(m::self()); 142 | $brandField = [ 143 | 'ID' => 1, 144 | 'NAME' => 'Название', 145 | ]; 146 | $bxObject->shouldReceive('Fetch')->times(2)->andReturn( 147 | $brandField, 148 | false 149 | ); 150 | TestElement::$bxObject = $bxObject; 151 | 152 | $this->assertEquals($brandField, $product->element->fields); 153 | 154 | // Проверка, что не выполняются дополнительные запросы 155 | $product->element; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/SectionModelTest.php: -------------------------------------------------------------------------------- 1 | 'Section one']); 24 | 25 | $this->assertSame(2, $section->id); 26 | $this->assertSame(['NAME' => 'Section one'], $section->fields); 27 | } 28 | 29 | public function testDelete() 30 | { 31 | $bxObject = m::mock('obj'); 32 | $bxObject->shouldReceive('delete')->once()->andReturn(true); 33 | 34 | TestSection::$bxObject = $bxObject; 35 | $section = new TestSection(1); 36 | 37 | $this->assertTrue($section->delete()); 38 | } 39 | 40 | public function testActivate() 41 | { 42 | $bxObject = m::mock('obj'); 43 | $bxObject->shouldReceive('update')->with(1, ['ACTIVE' => 'Y'], true, true, false)->once()->andReturn(true); 44 | 45 | TestSection::$bxObject = $bxObject; 46 | $section = new TestSection(1); 47 | 48 | $this->assertTrue($section->activate()); 49 | } 50 | 51 | public function testDeactivate() 52 | { 53 | $bxObject = m::mock('obj'); 54 | $bxObject->shouldReceive('update')->with(1, ['ACTIVE' => 'N'], true, true, false)->once()->andReturn(true); 55 | 56 | TestSection::$bxObject = $bxObject; 57 | $section = new TestSection(1); 58 | 59 | $this->assertTrue($section->deactivate()); 60 | } 61 | 62 | public function testCreate() 63 | { 64 | $bxObject = m::mock('obj'); 65 | $bxObject->shouldReceive('add')->with(['NAME' => 'Section 1', 'IBLOCK_ID' => TestSection::iblockId()], true, true, false)->once()->andReturn(3); 66 | 67 | TestSection::$bxObject = $bxObject; 68 | 69 | $newTestSection = TestSection::create(['NAME' => 'Section 1']); 70 | 71 | $this->assertSame(3, $newTestSection->id); 72 | $this->assertEquals([ 73 | 'NAME' => 'Section 1', 74 | 'ID' => 3, 75 | 'IBLOCK_ID' => TestSection::iblockId(), 76 | ], $newTestSection->fields); 77 | } 78 | 79 | public function testUpdate() 80 | { 81 | $section = m::mock('Arrilot\Tests\BitrixModels\Stubs\TestSection[save]', [1]); 82 | $section->shouldReceive('save')->with(['NAME', 'UF_FOO'])->andReturn(true); 83 | 84 | $this->assertTrue($section->update(['NAME' => 'Section 1', 'UF_FOO' => 'bar'])); 85 | $this->assertSame('Section 1', $section->fields['NAME']); 86 | $this->assertSame('bar', $section->fields['UF_FOO']); 87 | } 88 | 89 | public function testFill() 90 | { 91 | TestSection::$bxObject = m::mock('obj'); 92 | $section = new TestSection(1); 93 | 94 | $fields = ['ID' => 2, 'NAME' => 'Section 1']; 95 | $section->fill($fields); 96 | 97 | $this->assertSame(2, $section->id); 98 | $this->assertSame($fields, $section->fields); 99 | $this->assertSame($fields, $section->get()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/SectionQueryTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getList')->with( 35 | ['SORT' => 'ASC'], 36 | ['NAME' => 'John', 'ACTIVE' => 'Y', 'IBLOCK_ID' => 1], 37 | false, 38 | ['ID', 'NAME'], 39 | false 40 | )->once()->andReturn(m::self()); 41 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 42 | 43 | $query = $this->createQuery($bxObject); 44 | $items = $query->sort(['SORT' => 'ASC'])->filter(['NAME' => 'John'])->active()->select('ID', 'NAME')->getList(); 45 | 46 | $expected = [ 47 | 1 => ['ID' => 1, 'NAME' => 'foo'], 48 | 2 => ['ID' => 2, 'NAME' => 'bar'], 49 | ]; 50 | foreach ($items as $k => $item) { 51 | $this->assertSame($expected[$k], $item->toArray()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Stubs/BxUserWithAuth.php: -------------------------------------------------------------------------------- 1 | hasMany(TestElement2::class, 'ID', 'PROPERTY_ELEMENT_VALUE'); 46 | } 47 | 48 | public function element() 49 | { 50 | return $this->hasOne(TestElement2::class, 'PROPERTY_ELEMENT_VALUE', 'ID'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Stubs/TestElement2.php: -------------------------------------------------------------------------------- 1 | 'John Doe']); 27 | 28 | $this->assertSame(2, $user->id); 29 | $this->assertSame(['NAME' => 'John Doe'], $user->fields); 30 | 31 | $user = new TestUser(2, ['NAME' => 'John Doe', 'GROUP_ID' => [1, 2]]); 32 | 33 | $this->assertSame(2, $user->id); 34 | $this->assertSame(['NAME' => 'John Doe', 'GROUP_ID' => [1, 2]], $user->get()); 35 | $this->assertSame([1, 2], $user->getGroups()); 36 | } 37 | 38 | public function testCurrentWithAuth() 39 | { 40 | $GLOBALS['USER'] = new BxUserWithAuth(); 41 | global $USER; 42 | 43 | $this->mockLoadCurrentUserMethods(); 44 | 45 | $user = TestUser::freshCurrent(); 46 | $this->assertSame($USER->getId(), $user->id); 47 | $this->assertSame(['ID' => 1, 'NAME' => 'John Doe', 'GROUP_ID' => [1, 2, 3]], $user->fields); 48 | $this->assertSame([1, 2, 3], $user->getGroups()); 49 | } 50 | 51 | public function testCurrentWithoutAuth() 52 | { 53 | $GLOBALS['USER'] = new BxUserWithoutAuth(); 54 | 55 | $user = TestUser::freshCurrent(); 56 | $this->assertSame(null, $user->id); 57 | $this->assertSame([], $user->fields); 58 | } 59 | 60 | public function testHasRoleWithId() 61 | { 62 | $GLOBALS['USER'] = new BxUserWithoutAuth(); 63 | 64 | $user = TestUser::freshCurrent(); 65 | $this->assertFalse($user->hasGroupWithId(1)); 66 | 67 | $user = new TestUser(2, ['NAME' => 'John Doe', 'GROUP_ID' => [1, 2]]); 68 | 69 | $this->assertTrue($user->hasGroupWithId(1)); 70 | $this->assertTrue($user->hasGroupWithId(2)); 71 | $this->assertFalse($user->hasGroupWithId(3)); 72 | } 73 | 74 | public function testIsCurrent() 75 | { 76 | $GLOBALS['USER'] = new BxUserWithAuth(); 77 | 78 | $this->mockLoadCurrentUserMethods(); 79 | 80 | $user = TestUser::freshCurrent(); 81 | $this->assertTrue($user->isCurrent()); 82 | 83 | $user = new TestUser(1); 84 | $this->assertTrue($user->isCurrent()); 85 | 86 | $user = new TestUser(263); 87 | $this->assertFalse($user->isCurrent()); 88 | } 89 | 90 | public function testIsAuthorized() 91 | { 92 | $GLOBALS['USER'] = new BxUserWithAuth(); 93 | $this->mockLoadCurrentUserMethods(); 94 | $user = TestUser::freshCurrent(); 95 | $this->assertTrue($user->isAuthorized()); 96 | 97 | $GLOBALS['USER'] = new BxUserWithoutAuth(); 98 | $user = TestUser::freshCurrent(); 99 | $this->assertFalse($user->isAuthorized()); 100 | } 101 | 102 | public function testIsGuest() 103 | { 104 | $GLOBALS['USER'] = new BxUserWithAuth(); 105 | $this->mockLoadCurrentUserMethods(); 106 | $user = TestUser::freshCurrent(); 107 | $this->assertFalse($user->isGuest()); 108 | 109 | $GLOBALS['USER'] = new BxUserWithoutAuth(); 110 | $user = TestUser::freshCurrent(); 111 | $this->assertTrue($user->isGuest()); 112 | } 113 | 114 | public function testDelete() 115 | { 116 | $bxObject = m::mock('obj'); 117 | $bxObject->shouldReceive('delete')->once()->andReturn(true); 118 | 119 | TestUser::$bxObject = $bxObject; 120 | $user = new TestUser(1); 121 | 122 | $this->assertTrue($user->delete()); 123 | } 124 | 125 | public function testActivate() 126 | { 127 | $bxObject = m::mock('obj'); 128 | $bxObject->shouldReceive('update')->with(1, ['ACTIVE' => 'Y'])->once()->andReturn(true); 129 | 130 | TestUser::$bxObject = $bxObject; 131 | $user = new TestUser(1); 132 | 133 | $this->assertTrue($user->activate()); 134 | } 135 | 136 | public function testDeactivate() 137 | { 138 | $bxObject = m::mock('obj'); 139 | $bxObject->shouldReceive('update')->with(1, ['ACTIVE' => 'N'])->once()->andReturn(true); 140 | 141 | TestUser::$bxObject = $bxObject; 142 | $user = new TestUser(1); 143 | 144 | $this->assertTrue($user->deactivate()); 145 | } 146 | 147 | public function testCreate() 148 | { 149 | $bxObject = m::mock('obj'); 150 | $bxObject->shouldReceive('add')->with(['NAME' => 'John Doe'])->once()->andReturn(3); 151 | 152 | TestUser::$bxObject = $bxObject; 153 | 154 | $newTestUser = TestUser::create(['NAME' => 'John Doe']); 155 | 156 | $this->assertSame(3, $newTestUser->id); 157 | $this->assertSame([ 158 | 'NAME' => 'John Doe', 159 | 'ID' => 3, 160 | ], $newTestUser->fields); 161 | } 162 | 163 | public function testCount() 164 | { 165 | $bxObject = m::mock('obj'); 166 | $bxObject->shouldReceive('getList')->with('ID', 'ASC', ['ACTIVE' => 'Y'], [ 167 | 'NAV_PARAMS' => [ 168 | 'nTopCount' => 0, 169 | ], 170 | ])->once()->andReturn(m::self()); 171 | $bxObject->NavRecordCount = 2; 172 | 173 | TestUser::$bxObject = $bxObject; 174 | 175 | $this->assertSame(2, TestUser::count(['ACTIVE' => 'Y'])); 176 | 177 | $bxObject = m::mock('obj'); 178 | $bxObject->shouldReceive('getList')->with('ID', 'ASC', [], [ 179 | 'NAV_PARAMS' => [ 180 | 'nTopCount' => 0, 181 | ], 182 | ])->once()->andReturn(m::self()); 183 | $bxObject->NavRecordCount = 3; 184 | 185 | TestUser::$bxObject = $bxObject; 186 | 187 | $this->assertSame(3, TestUser::count()); 188 | } 189 | 190 | public function testUpdate() 191 | { 192 | $user = m::mock('Arrilot\Tests\BitrixModels\Stubs\TestUser[save]', [1]); 193 | $user->shouldReceive('save')->with(['NAME', 'UF_FOO'])->andReturn(true); 194 | 195 | $this->assertTrue($user->update(['NAME' => 'John', 'UF_FOO' => 'bar'])); 196 | $this->assertSame('John', $user->fields['NAME']); 197 | $this->assertSame('bar', $user->fields['UF_FOO']); 198 | } 199 | 200 | public function testFill() 201 | { 202 | $user = new TestUser(1); 203 | 204 | $fields = ['ID' => 2, 'NAME' => 'John Doe', 'GROUP_ID' => [1, 2]]; 205 | $user->fill($fields); 206 | 207 | $this->assertSame(2, $user->id); 208 | $this->assertSame($fields, $user->fields); 209 | $this->assertSame($fields, $user->get()); 210 | 211 | $bxObject = m::mock('obj'); 212 | $bxObject->shouldReceive('getUserGroup')->once()->andReturn([1]); 213 | TestUser::$bxObject = $bxObject; 214 | $user = new TestUser(1); 215 | 216 | $fields = ['ID' => 2, 'NAME' => 'John Doe']; 217 | $user->fill($fields); 218 | 219 | $this->assertSame(2, $user->id); 220 | $this->assertSame($fields, $user->fields); 221 | $this->assertSame($fields + ['GROUP_ID' => [1]], $user->get()); 222 | } 223 | 224 | public function testFillGroups() 225 | { 226 | $user = new TestUser(1); 227 | 228 | $user->fillGroups([1, 2]); 229 | 230 | $this->assertSame(1, $user->id); 231 | $this->assertSame(['GROUP_ID' => [1, 2]], $user->fields); 232 | $this->assertSame([1, 2], $user->getGroups()); 233 | } 234 | 235 | protected function mockLoadCurrentUserMethods() 236 | { 237 | $bxObject = m::mock('obj'); 238 | $bxObject->shouldReceive('getList')->withAnyArgs()->once()->andReturn(m::self()); 239 | $bxObject->shouldReceive('Fetch')->twice()->andReturn(['ID' => 1, 'NAME' => 'John Doe', 'GROUP_ID' => [1, 2, 3]], false); 240 | 241 | TestUser::$bxObject = $bxObject; 242 | } 243 | 244 | public function testItCanWorkAsAStartingPointForAQuery() 245 | { 246 | $this->assertInstanceOf(UserQuery::class, TestUser::query()); 247 | } 248 | 249 | public function testItCanWorkAsAStaticProxy() 250 | { 251 | $this->assertInstanceOf(UserQuery::class, TestUser::filter(['ACTIVE' => 'Y'])); 252 | $this->assertInstanceOf(UserQuery::class, TestUser::select('ID')); 253 | $this->assertInstanceOf(UserQuery::class, TestUser::take(15)); 254 | $this->assertInstanceOf(UserQuery::class, TestUser::forPage(1, 22)); 255 | $this->assertInstanceOf(UserQuery::class, TestUser::active()); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /tests/UserQueryTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getList')->with('ID', 'ASC', [], [ 34 | 'NAV_PARAMS' => [ 35 | 'nTopCount' => 0, 36 | ], 37 | ])->once()->andReturn(m::self()); 38 | $bxObject->NavRecordCount = 6; 39 | 40 | $query = $this->createQuery($bxObject); 41 | $count = $query->count(); 42 | 43 | $this->assertSame(6, $count); 44 | 45 | $bxObject = m::mock('obj'); 46 | $bxObject->shouldReceive('getList')->with('ID', 'ASC', ['ACTIVE' => 'Y'], [ 47 | 'NAV_PARAMS' => [ 48 | 'nTopCount' => 0, 49 | ], 50 | ])->once()->andReturn(m::self()); 51 | $bxObject->NavRecordCount = 3; 52 | 53 | $query = $this->createQuery($bxObject); 54 | $count = $query->filter(['ACTIVE' => 'Y'])->count(); 55 | 56 | $this->assertSame(3, $count); 57 | } 58 | 59 | public function testGetListWithScopes() 60 | { 61 | $bxObject = m::mock('obj'); 62 | TestUser::$bxObject = $bxObject; 63 | $bxObject->shouldReceive('getList')->with( 64 | ['SORT' => 'ASC'], 65 | false, 66 | ['NAME' => 'John', 'ACTIVE' => 'Y'], 67 | [ 68 | 'SELECT' => false, 69 | 'NAV_PARAMS' => false, 70 | 'FIELDS' => ['ID', 'NAME'], 71 | ] 72 | )->once()->andReturn(m::self()); 73 | $bxObject->shouldReceive('Fetch')->andReturn(['ID' => 1, 'NAME' => 'foo'], ['ID' => 2, 'NAME' => 'bar'], false); 74 | 75 | $query = $this->createQuery($bxObject); 76 | $items = $query->sort(['SORT' => 'ASC'])->filter(['NAME' => 'John'])->active()->select('ID', 'NAME')->getList(); 77 | 78 | $expected = [ 79 | 1 => ['ID' => 1, 'NAME' => 'foo'], 80 | 2 => ['ID' => 2, 'NAME' => 'bar'], 81 | ]; 82 | foreach ($items as $k => $item) { 83 | $this->assertSame($expected[$k], $item->toArray()); 84 | } 85 | } 86 | 87 | public function testGetByLogin() 88 | { 89 | $bxObject = m::mock('obj'); 90 | TestUser::$bxObject = $bxObject; 91 | $bxObject->shouldReceive('getList')->with( 92 | ['SORT' => 'ASC'], 93 | false, 94 | ['LOGIN_EQUAL_EXACT' => 'JohnDoe'], 95 | [ 96 | 'SELECT' => false, 97 | 'NAV_PARAMS' => ['nPageSize' => 1], 98 | 'FIELDS' => ['ID', 'NAME'], 99 | ] 100 | )->once()->andReturn(m::self()); 101 | $bxObject->shouldReceive('Fetch')->times(2)->andReturn(['ID' => 1, 'NAME' => 'foo'], false); 102 | 103 | $query = $this->createQuery($bxObject); 104 | $item = $query->sort(['SORT' => 'ASC'])->select('ID', 'NAME')->getByLogin('JohnDoe'); 105 | 106 | $this->assertSame(['ID' => 1, 'NAME' => 'foo'], $item->toArray()); 107 | } 108 | 109 | public function testGetByEmail() 110 | { 111 | $bxObject = m::mock('obj'); 112 | TestUser::$bxObject = $bxObject; 113 | $bxObject->shouldReceive('getList')->with( 114 | ['SORT' => 'ASC'], 115 | false, 116 | ['EMAIL' => 'john@example.com'], 117 | [ 118 | 'SELECT' => false, 119 | 'NAV_PARAMS' => ['nPageSize' => 1], 120 | 'FIELDS' => ['ID', 'NAME'], 121 | ] 122 | )->once()->andReturn(m::self()); 123 | $bxObject->shouldReceive('Fetch')->times(2)->andReturn(['ID' => 1, 'NAME' => 'foo'], false); 124 | 125 | $query = $this->createQuery($bxObject); 126 | $item = $query->sort(['SORT' => 'ASC'])->select('ID', 'NAME')->getByEmail('john@example.com'); 127 | 128 | $this->assertSame(['ID' => 1, 'NAME' => 'foo'], $item->toArray()); 129 | } 130 | } 131 | --------------------------------------------------------------------------------