├── .github └── workflows │ └── php-unit.yml ├── .gitignore ├── .scrutinizer.yml ├── LICENSE ├── Makefile ├── README.md ├── _config.yml ├── composer.json ├── docker-compose.yml ├── phpunit.xml ├── src ├── Annotation │ ├── AnnotationReader.php │ ├── Entity │ │ └── InfoBlock.php │ └── Property │ │ ├── AbstractPropertyAnnotation.php │ │ ├── Field.php │ │ ├── Property.php │ │ └── PropertyAnnotationInterface.php ├── EntityMapper.php ├── Map │ ├── EntityMap.php │ └── PropertyMap.php ├── Query │ ├── DataBuilder.php │ ├── FilterBuilder.php │ ├── RawResult.php │ └── Select.php └── SchemaBuilder.php └── tests ├── bootstrap.php ├── resources ├── Entity │ ├── Author.php │ ├── Book.php │ ├── WithConflictPropertyAnnotations.php │ └── WithoutInfoBlockAnnotation.php └── cover.jpg └── src ├── FunctionalTest ├── BitrixEnvironmentTest.php ├── EntityMapTest.php ├── EntityMapperTest.php ├── SchemaBuilderTest.php └── SelectTest.php ├── TestCase.php └── UnitTest ├── Annotation └── Property │ └── FieldTest.php ├── EntityMapperTest.php ├── Map └── EntityMapTest.php ├── Query ├── DataBuilderTest.php ├── RawResultTest.php └── SelectTest.php └── SchemaBuilderTest.php /.github/workflows/php-unit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | on: [ push ] 3 | jobs: 4 | PHPUnit: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | PHP: [ "7.3", "7.4", "8.0", "8.1", "8.2", "8.3" ] 9 | Bitrix: [ "18.5", "19.0", "20.0", "20.5", "21.400", "22.600" ] 10 | exclude: 11 | - { PHP: "7.3", Bitrix: "22.600" } 12 | - { PHP: "8.0", Bitrix: "18.5" } 13 | - { PHP: "8.0", Bitrix: "19.0" } 14 | - { PHP: "8.0", Bitrix: "20.0" } 15 | - { PHP: "8.0", Bitrix: "20.5" } 16 | - { PHP: "8.0", Bitrix: "21.400" } 17 | - { PHP: "8.1", Bitrix: "18.5" } 18 | - { PHP: "8.1", Bitrix: "19.0" } 19 | - { PHP: "8.1", Bitrix: "20.0" } 20 | - { PHP: "8.1", Bitrix: "20.5" } 21 | - { PHP: "8.1", Bitrix: "21.400" } 22 | - { PHP: "8.2", Bitrix: "18.5" } 23 | - { PHP: "8.2", Bitrix: "19.0" } 24 | - { PHP: "8.2", Bitrix: "20.0" } 25 | - { PHP: "8.2", Bitrix: "20.5" } 26 | - { PHP: "8.2", Bitrix: "21.400" } 27 | - { PHP: "8.3", Bitrix: "18.5" } 28 | - { PHP: "8.3", Bitrix: "19.0" } 29 | - { PHP: "8.3", Bitrix: "20.0" } 30 | - { PHP: "8.3", Bitrix: "20.5" } 31 | - { PHP: "8.3", Bitrix: "21.400" } 32 | container: 33 | image: webdevops/php-dev:${{ matrix.PHP }} 34 | env: 35 | MYSQL_HOST: mysql 36 | MYSQL_DATABASE: entity-mapper 37 | MYSQL_USER: entity-mapper 38 | MYSQL_PASSWORD: entity-mapper 39 | services: 40 | mysql: 41 | image: mysql:5.7 42 | env: 43 | MYSQL_RANDOM_ROOT_PASSWORD: "yes" 44 | MYSQL_DATABASE: entity-mapper 45 | MYSQL_USER: entity-mapper 46 | MYSQL_PASSWORD: entity-mapper 47 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 48 | steps: 49 | - name: Checkout source 50 | uses: actions/checkout@v1 51 | 52 | - name: Fix for https://github.com/actions/runner-images/issues/6775 53 | run: | 54 | chown root:root . 55 | git config --global --add safe.directory /__w/bitrix-entity-mapper/bitrix-entity-mapper 56 | 57 | - name: Set up environment 58 | run: | 59 | echo 'short_open_tag=1' >> /opt/docker/etc/php/php.ini 60 | echo 'mbstring.func_overload=2' >> /opt/docker/etc/php/php.ini 61 | cat /opt/docker/etc/php/php.ini 62 | 63 | - name: Install Bitrix CI 64 | run: composer require bitrix-toolkit/bitrix-ci:${{ matrix.Bitrix }} --dev --no-ansi --no-interaction --no-progress --prefer-dist 65 | 66 | - name: Install dependencies 67 | run: composer install --no-ansi --no-interaction --no-progress --prefer-dist 68 | 69 | - name: Run PHPUnit tests 70 | run: XDEBUG_MODE=coverage vendor/bin/phpunit --whitelist src/ --coverage-text --coverage-clover clover.xml 71 | 72 | - name: Upload coverage to https://scrutinizer-ci.com 73 | if: matrix.PHP == '7.4' && matrix.Bitrix == '21.400' 74 | continue-on-error: true 75 | run: | 76 | wget -nv https://scrutinizer-ci.com/ocular.phar 77 | php ocular.phar code-coverage:upload --format=php-clover clover.xml 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /var/ 3 | /composer.lock 4 | *.zip 5 | /public/ 6 | /files/bitrix/managed_cache/ 7 | *.php~ 8 | /.idea/ 9 | .phpunit.result.cache -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 7.4.33 4 | nodes: 5 | analysis: 6 | tests: 7 | override: 8 | - php-scrutinizer-run 9 | filter: 10 | excluded_paths: 11 | - "tests/" 12 | dependency_paths: 13 | - "vendor/" 14 | checks: 15 | php: true 16 | tools: 17 | external_code_coverage: true 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | composer install --prefer-dist --no-interaction 3 | 4 | test: 5 | php vendor/bin/phpunit 6 | 7 | coverage: 8 | XDEBUG_MODE=coverage php vendor/bin/phpunit --whitelist src/ --coverage-text 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitrix Entity Mapper 2 | 3 | [![PHPUnit](https://github.com/bitrix-toolkit/bitrix-entity-mapper/actions/workflows/php-unit.yml/badge.svg)](https://github.com/bitrix-toolkit/bitrix-entity-mapper/actions/workflows/php-unit.yml) 4 | [![Coverage](https://scrutinizer-ci.com/g/bitrix-toolkit/bitrix-entity-mapper/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/bitrix-toolkit/bitrix-entity-mapper/?branch=master) 5 | [![Scrutinizer](https://scrutinizer-ci.com/g/bitrix-toolkit/bitrix-entity-mapper/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/bitrix-toolkit/bitrix-entity-mapper/?branch=master) 6 | 7 | Декларативный ORM для инфоблоков Bitrix. 8 | 9 | ## Установка 10 | 11 | ```bash 12 | composer require bitrix-toolkit/bitrix-entity-mapper 13 | ``` 14 | 15 | ## Быстрый старт 16 | 17 | Описываем с помощью PHPDoc аннотаций способ хранения объектов в Bitrix: 18 | 19 | ```php 20 | active = true; 88 | $book->title = 'Остров сокровищ'; 89 | $book->author = 'Р. Л. Стивенсон'; 90 | $book->publishedAt = new DateTime('1883-06-14 00:00:00'); 91 | 92 | $bitrixId = EntityMapper::save($book); 93 | ``` 94 | 95 | Есть несколько способов перебрать результат: 96 | 97 | ```php 98 | use BitrixToolkit\BitrixEntityMapper\EntityMapper; 99 | use Entity\Book; 100 | 101 | $query = EntityMapper::select(Book::class)->where('author', 'Р. Л. Стивенсон'); 102 | 103 | // Получить один результат. 104 | $query->fetch(); 105 | 106 | // Перебрать по одному результату. 107 | while ($book = $query->fetch()) { /* ... */ } 108 | 109 | // Использовать реализованную имплементацию интерфейса Iterator. 110 | foreach ($query as $book) { /* ... */ } 111 | 112 | // Использовать метод возвращающий генератор. 113 | foreach ($query->iterator() as $book) { /* ... */ } 114 | 115 | // Получить массив со всеми результатами. 116 | // Не рекомендуется! Небезопасное потребление памяти. 117 | $query->fetchAll(); 118 | ``` 119 | 120 | Получаем результат по фильтру сущности: 121 | 122 | ```php 123 | use BitrixToolkit\BitrixEntityMapper\EntityMapper; 124 | use Entity\Book; 125 | 126 | /** @var Book|null $book */ 127 | $book = EntityMapper::select(Book::class)->where('title', 'Остров сокровищ')->fetch(); 128 | 129 | /** @var Book[] $books */ 130 | $books = EntityMapper::select(Book::class)->where('author', '%', 'Стивенсон')->fetchAll(); 131 | 132 | /** @var Book[] $books */ 133 | $books = EntityMapper::select(Book::class)->where('publishedAt', '<', '01.01.1900')->fetchAll(); 134 | ``` 135 | 136 | Получаем результат по фильтру Bitrix: 137 | 138 | ```php 139 | use BitrixToolkit\BitrixEntityMapper\EntityMapper; 140 | use Entity\Book; 141 | 142 | /** @var Book|null $book */ 143 | $book = EntityMapper::select(Book::class)->whereRaw('ID', 1)->fetch(); 144 | 145 | /** @var Book[] $books */ 146 | $books = EntityMapper::select(Book::class)->whereRaw('ACTIVE', 'Y')->fetchAll(); 147 | ``` 148 | 149 | Сортируем выборку: 150 | 151 | ```php 152 | use BitrixToolkit\BitrixEntityMapper\EntityMapper; 153 | use Entity\Book; 154 | 155 | /** @var Book|null $book */ 156 | $book = EntityMapper::select(Book::class)->orderBy('publishedAt', 'desc')->fetch(); 157 | ``` 158 | 159 | Обновляем существующий объект: 160 | 161 | ```php 162 | use BitrixToolkit\BitrixEntityMapper\EntityMapper; 163 | use Entity\Book; 164 | 165 | /** @var Book|null $existBook */ 166 | $existBook = EntityMapper::select(Book::class)->fetch(); 167 | 168 | if ($existBook) { 169 | $existBook->title = 'Забытая книга'; 170 | $existBook->author = 'Неизвестный автор'; 171 | $existBook->publishedAt = null; 172 | $existBook->active = false; 173 | $updatedBitrixId = EntityMapper::save($existBook); 174 | } 175 | ``` 176 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitrix-toolkit/bitrix-entity-mapper", 3 | "description": "Alternative ORM for Bitrix", 4 | "type": "library", 5 | "license": "Unlicense", 6 | "authors": [ 7 | { 8 | "name": "Vitalik Shirokov", 9 | "email": "vitalik.shirokov@gmail.com" 10 | }, 11 | { 12 | "name": "Vitaly Artemyev", 13 | "email": "mail@vitalyart.ru" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "BitrixToolkit\\BitrixEntityMapper\\": "src/" 19 | } 20 | }, 21 | "require": { 22 | "php": ">=7.3", 23 | "ext-mysqli": "*", 24 | "doctrine/annotations": "^1.4" 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "BitrixToolkit\\BitrixEntityMapper\\Test\\": "tests/src/" 29 | } 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^9.6", 33 | "symfony/var-dumper": "^5.4", 34 | "bitrix-toolkit/bitrix-ci": "18.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bitrix: 3 | image: docker.io/webdevops/php-apache-dev:${PHP-8.3} 4 | volumes: 5 | - ./:/app/ 6 | working_dir: /app/ 7 | depends_on: 8 | - mysql 9 | ports: 10 | - ${HTTP_PORT-8080}:80 11 | environment: 12 | WEB_DOCUMENT_ROOT: /app/public/ 13 | php.short_open_tag: 1 14 | php.display_errors: 0 15 | php.max_input_vars: 10000 16 | php.memory_limit: "256M" 17 | php.date.timezone: "Europe/Moscow" 18 | MYSQL_HOST: "mysql" 19 | MYSQL_DATABASE: "bitrix-ci" 20 | MYSQL_USER: "bitrix-ci" 21 | MYSQL_PASSWORD: "bitrix-ci" 22 | DEBUG: 1 23 | POSTFIX_RELAYHOST: "[mailhog]:1025" 24 | mysql: 25 | image: docker.io/library/mysql:5.7 26 | volumes: 27 | - ./var/mysql/:/var/lib/mysql/ 28 | ports: 29 | - ${MYSQL_PORT-3306}:3306 30 | command: >- 31 | --default-time-zone=Europe/Moscow 32 | --character-set-server=utf8 33 | --collation-server=utf8_unicode_ci 34 | --skip-innodb-strict-mode 35 | environment: 36 | MYSQL_RANDOM_ROOT_PASSWORD: "yes" 37 | MYSQL_DATABASE: "bitrix-ci" 38 | MYSQL_USER: "bitrix-ci" 39 | MYSQL_PASSWORD: "bitrix-ci" 40 | mailhog: 41 | image: docker.io/mailhog/mailhog 42 | ports: 43 | - ${MAILHOG_HTTP_PORT-8025}:8025 44 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/src/FunctionalTest/BitrixEnvironmentTest.php 5 | tests/src/FunctionalTest/EntityMapTest.php 6 | tests/src/FunctionalTest/SchemaBuilderTest.php 7 | tests/src/FunctionalTest/SelectTest.php 8 | tests/src/FunctionalTest/EntityMapperTest.php 9 | 10 | 11 | tests/src/UnitTest/EntityMapperTest.php 12 | tests/src/UnitTest/SchemaBuilderTest.php 13 | tests/src/UnitTest/Annotation/Property/FieldTest.php 14 | tests/src/UnitTest/Map/EntityMapTest.php 15 | tests/src/UnitTest/Query/RawResultTest.php 16 | tests/src/UnitTest/Query/SelectTest.php 17 | tests/src/UnitTest/Query/DataBuilderTest.php 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Annotation/AnnotationReader.php: -------------------------------------------------------------------------------- 1 | type = isset($values['type']) ? $values['type'] : null; 38 | $this->code = isset($values['code']) ? $values['code'] : null; 39 | $this->name = isset($values['name']) ? $values['name'] : null; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getType() 46 | { 47 | return $this->type; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getCode() 54 | { 55 | return $this->code; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getName() 62 | { 63 | return $this->name; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Annotation/Property/AbstractPropertyAnnotation.php: -------------------------------------------------------------------------------- 1 | code; 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getType() 26 | { 27 | return $this->type; 28 | } 29 | 30 | /** 31 | * @return bool 32 | */ 33 | public function isMultiple() 34 | { 35 | return $this->multiple; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getName() 42 | { 43 | return $this->name; 44 | } 45 | 46 | /** 47 | * @return bool 48 | */ 49 | public function isPrimaryKey() 50 | { 51 | return $this->primaryKey; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getEntity() 58 | { 59 | return $this->entity; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Annotation/Property/Field.php: -------------------------------------------------------------------------------- 1 | code = isset($values['code']) ? $values['code'] : null; 31 | $this->primaryKey = isset($values['primaryKey']) ? (bool)$values['primaryKey'] : false; 32 | $this->type = self::getTypeByCode($this->code); 33 | } 34 | 35 | /** 36 | * @param string $code 37 | * @return string 38 | */ 39 | private static function getTypeByCode($code) 40 | { 41 | $map = [ 42 | 'ID' => self::TYPE_INTEGER, 43 | 'SORT' => self::TYPE_INTEGER, 44 | 'ACTIVE' => self::TYPE_BOOLEAN, 45 | 'DATE_ACTIVE_FROM' => self::TYPE_DATETIME, 46 | 'DATE_ACTIVE_TO' => self::TYPE_DATETIME, 47 | 'PREVIEW_PICTURE' => self::TYPE_FILE, 48 | 'DETAIL_PICTURE' => self::TYPE_FILE, 49 | 'SECTION_ID' => self::TYPE_INTEGER, 50 | ]; 51 | 52 | return array_key_exists($code, $map) ? $map[$code] : self::TYPE_STRING; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Annotation/Property/Property.php: -------------------------------------------------------------------------------- 1 | code = isset($values['code']) ? $values['code'] : null; 51 | $this->type = isset($values['type']) ? $values['type'] : null; 52 | $this->name = isset($values['name']) ? $values['name'] : null; 53 | $this->multiple = isset($values['multiple']) ? $values['multiple'] : null; 54 | $this->primaryKey = isset($values['primaryKey']) ? (bool)$values['primaryKey'] : false; 55 | $this->entity = isset($values['entity']) ? $values['entity'] : null; 56 | } 57 | 58 | /** 59 | * @return bool|null 60 | */ 61 | public function isMultiple() 62 | { 63 | return $this->multiple; 64 | } 65 | } -------------------------------------------------------------------------------- /src/Annotation/Property/PropertyAnnotationInterface.php: -------------------------------------------------------------------------------- 1 | getId()) { 44 | return self::update($exist, $entityMap, $data); 45 | } else { 46 | return self::add($entityMap, $data); 47 | } 48 | } 49 | 50 | /** 51 | * @param string $class 52 | * @return Select 53 | * @throws AnnotationException 54 | * @throws ReflectionException 55 | * @throws InvalidArgumentException 56 | */ 57 | public static function select($class) 58 | { 59 | return Select::from($class); 60 | } 61 | 62 | /** 63 | * @param EntityMap $entityMap 64 | * @param array $data 65 | * @return int 66 | * @throws Exception 67 | */ 68 | protected static function add(EntityMap $entityMap, array $data) 69 | { 70 | $bitrixFields = DataBuilder::getBitrixFields($entityMap, $data); 71 | $bitrixProperties = DataBuilder::getBitrixProperties($entityMap, $data, $bitrixFields['IBLOCK_ID']); 72 | 73 | $addFields = $bitrixFields; 74 | if (!empty($bitrixProperties)) { 75 | $addFields['PROPERTY_VALUES'] = $bitrixProperties; 76 | } 77 | 78 | $cIBlockElement = new CIBlockElement(); 79 | $elementId = $cIBlockElement->Add($addFields); 80 | self::assert($elementId, strip_tags($cIBlockElement->LAST_ERROR)); 81 | 82 | return $elementId; 83 | } 84 | 85 | /** 86 | * @param RawResult $exist 87 | * @param EntityMap $entityMap 88 | * @param array $data 89 | * @return int 90 | * @throws Exception 91 | */ 92 | protected static function update(RawResult $exist, EntityMap $entityMap, array $data) 93 | { 94 | $changedData = self::getChangedData($exist->getData(), $data); 95 | 96 | if (empty($changedData)) { 97 | return $exist->getId(); 98 | } 99 | 100 | self::updateBitrixFields($exist, $entityMap, $changedData); 101 | self::updateBitrixProperties($exist, $entityMap, $changedData); 102 | 103 | return $exist->getId(); 104 | } 105 | 106 | /** 107 | * @param array $exist 108 | * @param array $data 109 | * @return array 110 | */ 111 | protected static function getChangedData(array $exist, array $data) 112 | { 113 | return array_udiff_assoc($data, $exist, function ($new, $old) { 114 | $normalize = function ($value) { 115 | if ($value instanceof DateTime) { 116 | return $value->getTimestamp(); 117 | } 118 | 119 | return $value; 120 | }; 121 | 122 | $new = $new === null || $new === false || $new === [] ? false : array_map($normalize, (array)$new); 123 | $old = $old === null || $old === false || $old === [] ? false : array_map($normalize, (array)$old); 124 | 125 | return $new !== $old; 126 | }); 127 | } 128 | 129 | /** 130 | * @param RawResult $exist 131 | * @param EntityMap $entityMap 132 | * @param array $changedData 133 | * @throws Exception 134 | */ 135 | protected static function updateBitrixFields(RawResult $exist, EntityMap $entityMap, array $changedData) 136 | { 137 | $changedFields = array_filter($entityMap->getProperties(), function (PropertyMap $field) use ($changedData) { 138 | return ( 139 | $field->getAnnotation() instanceof Field && 140 | in_array($field->getReflection()->getName(), array_keys($changedData)) 141 | ); 142 | }); 143 | 144 | if (empty($changedFields)) { 145 | return; 146 | } 147 | 148 | $bitrixFields = []; 149 | foreach ($changedFields as $changedField) { 150 | $bitrixFields += DataBuilder::getBitrixFieldEntry($changedField, $changedData); 151 | } 152 | 153 | $cIBlockElement = new CIBlockElement(); 154 | $isUpdated = $cIBlockElement->Update($exist->getId(), $bitrixFields); 155 | self::assert($isUpdated, strip_tags($cIBlockElement->LAST_ERROR)); 156 | } 157 | 158 | /** 159 | * @param RawResult $exist 160 | * @param EntityMap $entityMap 161 | * @param array $changedData 162 | * @throws Exception 163 | */ 164 | protected static function updateBitrixProperties(RawResult $exist, EntityMap $entityMap, array $changedData) 165 | { 166 | $changedProperties = array_filter($entityMap->getProperties(), function (PropertyMap $property) use ($changedData) { 167 | return ( 168 | $property->getAnnotation() instanceof Property && 169 | in_array($property->getReflection()->getName(), array_keys($changedData)) 170 | ); 171 | }); 172 | 173 | if (empty($changedProperties)) { 174 | return; 175 | } 176 | 177 | $bitrixProperties = []; 178 | foreach ($changedProperties as $changedProperty) { 179 | $bitrixProperties += DataBuilder::getBitrixPropertyEntry($changedProperty, $changedData, $exist->getInfoBlockId()); 180 | } 181 | 182 | CIBlockElement::SetPropertyValuesEx($exist->getId(), $exist->getInfoBlockId(), $bitrixProperties); 183 | } 184 | 185 | /** 186 | * @param EntityMap $entityMap 187 | * @param array $data 188 | * @return array 189 | * @throws AnnotationException 190 | * @throws ReflectionException 191 | * @throws InvalidArgumentException 192 | */ 193 | protected static function saveChildEntities(EntityMap $entityMap, array $data) 194 | { 195 | /** @var PropertyMap[] $entityProperties */ 196 | $entityProperties = array_filter($entityMap->getProperties(), function (PropertyMap $propertyMap) { 197 | return $propertyMap->getAnnotation()->getType() === Property::TYPE_ENTITY; 198 | }); 199 | 200 | foreach ($entityProperties as $entityProperty) { 201 | self::checkChildEntity($entityProperty, $data); 202 | } 203 | 204 | $entityData = []; 205 | foreach ($entityProperties as $entityProperty) { 206 | $entityPropertyData = self::saveChildEntity($entityProperty, $data); 207 | $entityData += $entityPropertyData; 208 | } 209 | 210 | return $entityData; 211 | } 212 | 213 | /** 214 | * @param PropertyMap $entityProperty 215 | * @param array $data 216 | * @return array 217 | * @throws AnnotationException 218 | * @throws ReflectionException 219 | */ 220 | protected static function saveChildEntity(PropertyMap $entityProperty, array $data) 221 | { 222 | $entityData = []; 223 | $key = $entityProperty->getCode(); 224 | 225 | $rawValue = array_key_exists($key, $data) ? $data[$key] : null; 226 | if (empty($rawValue)) { 227 | $entityData[$key] = false; 228 | return $entityData; 229 | } 230 | 231 | if ($entityProperty->getAnnotation()->isMultiple()) { 232 | foreach ($rawValue as $object) { 233 | $objectId = self::save($object); 234 | $entityData[$key][] = $objectId; 235 | } 236 | } else { 237 | $objectId = self::save($rawValue); 238 | $entityData[$key] = $objectId; 239 | } 240 | 241 | return $entityData; 242 | } 243 | 244 | /** 245 | * @param PropertyMap $entityProperty 246 | * @param array $data 247 | * @throws InvalidArgumentException 248 | */ 249 | protected static function checkChildEntity(PropertyMap $entityProperty, array $data) 250 | { 251 | $key = $entityProperty->getCode(); 252 | self::assert(array_key_exists($key, $data), "Ключ $key не найден в массиве данных полученных из объекта."); 253 | $value = $data[$key]; 254 | 255 | if ($entityProperty->getAnnotation()->isMultiple()) { 256 | $objects = $value ? $value : []; 257 | self::assert(is_array($objects), 'Множественное значение должно быть массивом.'); 258 | } else { 259 | $objects = $value ? [$value] : []; 260 | } 261 | 262 | $needClass = $entityProperty->getAnnotation()->getEntity(); 263 | foreach ($objects as $object) { 264 | self::assert(is_object($object), 'Значение типа ' . Property::TYPE_ENTITY . ' должно быть объектом.'); 265 | self::assert($object instanceof $needClass, "Объект должен быть экземпляром класса $needClass."); 266 | } 267 | } 268 | 269 | /** 270 | * @param mixed $term 271 | * @param string $msg 272 | * @throws InvalidArgumentException 273 | */ 274 | protected static function assert($term, $msg) 275 | { 276 | if (!$term) { 277 | throw new InvalidArgumentException($msg); 278 | } 279 | } 280 | 281 | /** 282 | * @param EntityMap $entityMap 283 | * @param object $object 284 | * @return array 285 | */ 286 | protected static function entityToArray(EntityMap $entityMap, $object) 287 | { 288 | $data = []; 289 | foreach ($entityMap->getProperties() as $propertyMap) { 290 | if (!$propertyMap->getReflection()->isPublic()) { 291 | $propertyMap->getReflection()->setAccessible(true); 292 | $value = $propertyMap->getReflection()->getValue($object); 293 | $propertyMap->getReflection()->setAccessible(false); 294 | } else { 295 | $value = $propertyMap->getReflection()->getValue($object); 296 | } 297 | 298 | $data[$propertyMap->getReflection()->getName()] = $value; 299 | } 300 | 301 | return $data; 302 | } 303 | 304 | /** 305 | * @param EntityMap $entityMap 306 | * @param object $object 307 | * @return RawResult|null 308 | * @throws AnnotationException 309 | * @throws ReflectionException 310 | * @throws InvalidArgumentException 311 | * @throws Exception 312 | */ 313 | protected static function getExistObjectRawResult(EntityMap $entityMap, $object) 314 | { 315 | /** @var PropertyMap[] $primaryKeys */ 316 | $primaryKeys = array_filter($entityMap->getProperties(), function (PropertyMap $propertyMap) { 317 | return $propertyMap->getAnnotation()->isPrimaryKey(); 318 | }); 319 | 320 | $data = self::entityToArray($entityMap, $object); 321 | 322 | $exist = null; 323 | if (!empty($primaryKeys)) { 324 | $select = Select::from($entityMap->getClass()); 325 | foreach ($primaryKeys as $primaryKey) { 326 | $key = $primaryKey->getReflection()->getName(); 327 | self::assert(array_key_exists($key, $data), "Ключ $key не найден в массиве данных полученных из объекта."); 328 | $select->where($key, $data[$key]); 329 | } 330 | 331 | /** @var RawResult $exist */ 332 | $exist = $select->rawIterator()->current(); 333 | } 334 | 335 | return $exist; 336 | } 337 | } -------------------------------------------------------------------------------- /src/Map/EntityMap.php: -------------------------------------------------------------------------------- 1 | class = $class; 44 | $this->annotation = $annotation; 45 | $this->reflection = $reflection; 46 | $this->properties = $properties; 47 | } 48 | 49 | /** 50 | * @param string|object $class 51 | * @return EntityMap 52 | * @throws AnnotationException 53 | * @throws ReflectionException 54 | * @throws InvalidArgumentException 55 | */ 56 | public static function fromClass($class) 57 | { 58 | $class = is_object($class) ? get_class($class) : $class; 59 | $annotationReader = new AnnotationReader(); 60 | $classRef = new ReflectionClass($class); 61 | 62 | /** @var InfoBlock|null $classAnnotation */ 63 | $classAnnotation = $annotationReader->getClassAnnotation($classRef, InfoBlock::class); 64 | if (!$classAnnotation) { 65 | throw new InvalidArgumentException('Нет аннотации @' . InfoBlock::class . ' для класса ' . $classRef->getName() . '.'); 66 | } 67 | 68 | $propertyMaps = []; 69 | foreach ($classRef->getProperties() as $propRef) { 70 | $propertyMap = PropertyMap::fromReflectionProperty($propRef); 71 | if ($propertyMap) { 72 | $propertyMaps[] = $propertyMap; 73 | } 74 | } 75 | 76 | $entityMap = new self($classRef->getName(), $classAnnotation, $classRef, $propertyMaps); 77 | return $entityMap; 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getClass() 84 | { 85 | return $this->class; 86 | } 87 | 88 | /** 89 | * @return InfoBlock 90 | */ 91 | public function getAnnotation() 92 | { 93 | return $this->annotation; 94 | } 95 | 96 | /** 97 | * @return ReflectionClass 98 | */ 99 | public function getReflection() 100 | { 101 | return $this->reflection; 102 | } 103 | 104 | /** 105 | * @return PropertyMap[] 106 | */ 107 | public function getProperties() 108 | { 109 | return $this->properties; 110 | } 111 | 112 | /** 113 | * @param string $code 114 | * @return PropertyMap 115 | * @throws InvalidArgumentException 116 | */ 117 | public function getProperty($code) 118 | { 119 | foreach ($this->properties as $property) { 120 | if ($property->getCode() === $code) { 121 | return $property; 122 | } 123 | } 124 | 125 | throw new InvalidArgumentException("Свойство $code не объявлено в сущности."); 126 | } 127 | } -------------------------------------------------------------------------------- /src/Map/PropertyMap.php: -------------------------------------------------------------------------------- 1 | code = $code; 37 | $this->annotation = $annotation; 38 | $this->reflection = $reflection; 39 | } 40 | 41 | /** 42 | * @param ReflectionProperty $propRef 43 | * @return PropertyMap|null 44 | * @throws AnnotationException 45 | */ 46 | public static function fromReflectionProperty(ReflectionProperty $propRef) 47 | { 48 | $annotationReader = new AnnotationReader(); 49 | $propAnnotations = $annotationReader->getPropertyAnnotations($propRef); 50 | $propAnnotations = array_filter($propAnnotations, function ($propAnnotation) { 51 | return $propAnnotation instanceof PropertyAnnotationInterface; 52 | }); 53 | 54 | if (empty($propAnnotations)) { 55 | return null; 56 | } 57 | 58 | if (count($propAnnotations) > 1) { 59 | $annotationClasses = array_map(function ($propAnnotation) { 60 | return get_class($propAnnotation); 61 | }, $propAnnotations); 62 | 63 | throw new InvalidArgumentException( 64 | 'Аннотации ' . '@' . implode(', @', $annotationClasses) . 65 | ' свойства ' . $propRef->getName() . ' класса ' . $propRef->getDeclaringClass()->getName() . 66 | ' не могут быть применены одновременно.' 67 | ); 68 | } 69 | 70 | /** @var PropertyAnnotationInterface $propAnnotation */ 71 | $propAnnotation = reset($propAnnotations); 72 | return new PropertyMap($propRef->getName(), $propAnnotation, $propRef); 73 | } 74 | 75 | /** 76 | * @return string 77 | */ 78 | public function getCode() 79 | { 80 | return $this->code; 81 | } 82 | 83 | /** 84 | * @return PropertyAnnotationInterface 85 | */ 86 | public function getAnnotation() 87 | { 88 | return $this->annotation; 89 | } 90 | 91 | /** 92 | * @return ReflectionProperty 93 | */ 94 | public function getReflection() 95 | { 96 | return $this->reflection; 97 | } 98 | } -------------------------------------------------------------------------------- /src/Query/DataBuilder.php: -------------------------------------------------------------------------------- 1 | getProperties(), function (PropertyMap $propertyMap) { 28 | return $propertyMap->getAnnotation() instanceof Field; 29 | }); 30 | 31 | $infoBlockType = $entityMap->getAnnotation()->getType(); 32 | $infoBlockCode = $entityMap->getAnnotation()->getCode(); 33 | $infoBlock = self::getBitrixInfoBlock($infoBlockType, $infoBlockCode); 34 | self::assert(!empty($infoBlock['ID']), "Не найден инфоблок с кодом $infoBlockCode и типом $infoBlockType."); 35 | 36 | $bitrixFields = ['IBLOCK_ID' => $infoBlock['ID']]; 37 | foreach ($fields as $field) { 38 | $bitrixFields += self::getBitrixFieldEntry($field, $data); 39 | } 40 | 41 | return $bitrixFields; 42 | } 43 | 44 | /** 45 | * @param string $type 46 | * @param string $code 47 | * @return array 48 | */ 49 | protected static function getBitrixInfoBlock($type, $code) 50 | { 51 | return CIBlock::GetList(null, [ 52 | 'TYPE' => $type, 53 | 'CODE' => $code, 54 | 'CHECK_PERMISSIONS' => 'N' 55 | ])->Fetch(); 56 | } 57 | 58 | /** 59 | * @param mixed $term 60 | * @param string $msg 61 | * @throws InvalidArgumentException 62 | */ 63 | protected static function assert($term, $msg) 64 | { 65 | if (!$term) { 66 | throw new InvalidArgumentException($msg); 67 | } 68 | } 69 | 70 | /** 71 | * @param PropertyMap $propertyMap 72 | * @param array $data 73 | * @return array 74 | * @throws InvalidArgumentException 75 | */ 76 | public static function getBitrixFieldEntry(PropertyMap $propertyMap, array $data) 77 | { 78 | self::assert( 79 | $propertyMap->getAnnotation() instanceof Field, 80 | 'Аннотация свойства должна быть экземпляром ' . Field::class . '.' 81 | ); 82 | 83 | $key = $propertyMap->getAnnotation()->getCode(); 84 | 85 | $valueKey = $propertyMap->getReflection()->getName(); 86 | self::assert(array_key_exists($valueKey, $data), "Ключ $valueKey не найден в массиве."); 87 | $value = $data[$valueKey]; 88 | 89 | if ($propertyMap->getAnnotation()->getType() === PropertyAnnotationInterface::TYPE_BOOLEAN) { 90 | $value = $value ? 'Y' : 'N'; 91 | } 92 | 93 | return [$key => $value]; 94 | } 95 | 96 | /** 97 | * @param EntityMap $entityMap 98 | * @param array $data 99 | * @param int $infoBlockId 100 | * @return array 101 | * @throws Exception 102 | */ 103 | public static function getBitrixProperties(EntityMap $entityMap, array $data, $infoBlockId) 104 | { 105 | $properties = array_filter($entityMap->getProperties(), function (PropertyMap $propertyMap) { 106 | return $propertyMap->getAnnotation() instanceof Property; 107 | }); 108 | 109 | $bitrixProperties = []; 110 | foreach ($properties as $property) { 111 | $bitrixProperties += self::getBitrixPropertyEntry($property, $data, $infoBlockId); 112 | } 113 | 114 | return $bitrixProperties; 115 | } 116 | 117 | /** 118 | * @param PropertyMap $propertyMap 119 | * @param array $data 120 | * @param int $infoBlockId 121 | * @return array 122 | * @throws InvalidArgumentException 123 | * @throws Exception 124 | */ 125 | public static function getBitrixPropertyEntry(PropertyMap $propertyMap, array $data, $infoBlockId) 126 | { 127 | self::assert( 128 | $propertyMap->getAnnotation() instanceof Property, 129 | 'Аннотация свойства должна быть экземпляром ' . Property::class . '.' 130 | ); 131 | 132 | $key = $propertyMap->getAnnotation()->getCode(); 133 | 134 | $valueKey = $propertyMap->getReflection()->getName(); 135 | self::assert(array_key_exists($valueKey, $data), "Ключ $valueKey не найден в массиве."); 136 | $value = $data[$valueKey]; 137 | 138 | if ($propertyMap->getAnnotation()->isMultiple()) { 139 | $value = array_map(function ($value) use ($propertyMap, $infoBlockId) { 140 | return self::normalizeValueForBitrix($propertyMap, $value, $infoBlockId); 141 | }, (array)$value); 142 | } else { 143 | $value = self::normalizeValueForBitrix($propertyMap, $value, $infoBlockId); 144 | } 145 | 146 | return [$key => $value]; 147 | } 148 | 149 | /** 150 | * @param PropertyMap $propertyMap 151 | * @param mixed $value 152 | * @param int $infoBlockId 153 | * @return mixed 154 | * @throws Exception 155 | */ 156 | protected static function normalizeValueForBitrix(PropertyMap $propertyMap, $value, $infoBlockId) 157 | { 158 | if ($propertyMap->getAnnotation()->getType() === Property::TYPE_BOOLEAN) { 159 | return self::normalizeBooleanForBitrix($propertyMap->getAnnotation()->getCode(), $value, $infoBlockId); 160 | } elseif ($propertyMap->getAnnotation()->getType() === Property::TYPE_DATETIME) { 161 | return self::normalizeDateTimeForBitrix($value); 162 | } else { 163 | return $value; 164 | } 165 | } 166 | 167 | /** 168 | * @param string $code 169 | * @param mixed $value 170 | * @param int $infoBlockId 171 | * @return bool 172 | * @throws InvalidArgumentException 173 | */ 174 | protected static function normalizeBooleanForBitrix($code, $value, $infoBlockId) 175 | { 176 | if (!$value) { 177 | return false; 178 | } 179 | 180 | $yesEnum = CIBlockProperty::GetPropertyEnum($code, null, [ 181 | 'IBLOCK_ID' => $infoBlockId, 182 | 'XML_ID' => 'Y', 183 | 'VALUE' => 'Y' 184 | ])->Fetch(); 185 | 186 | self::assert( 187 | !empty($yesEnum['ID']), 188 | 'Не найден ID варианта ответа Y для булевого значения свойства ' . $code . '.' 189 | ); 190 | 191 | return $yesEnum['ID']; 192 | } 193 | 194 | /** 195 | * @param mixed $value 196 | * @return BitrixDateTime|bool 197 | * @throws Exception 198 | */ 199 | protected static function normalizeDateTimeForBitrix($value) 200 | { 201 | if (!$value) { 202 | return false; 203 | } 204 | 205 | if ($value instanceof BitrixDateTime) { 206 | return $value; 207 | } 208 | 209 | if ($value instanceof DateTime) { 210 | $dateTime = BitrixDateTime::createFromTimestamp($value->getTimestamp()); 211 | } elseif (preg_match('/^-?\d+$/us', (string)$value)) { 212 | $dateTime = BitrixDateTime::createFromTimestamp($value); 213 | } else { 214 | $dateTime = BitrixDateTime::createFromPhp(new DateTime($value)); 215 | } 216 | 217 | return $dateTime; 218 | } 219 | } -------------------------------------------------------------------------------- /src/Query/FilterBuilder.php: -------------------------------------------------------------------------------- 1 | entityMap = $entityMap; 23 | $this->where = $where; 24 | $this->whereRaw = $whereRaw; 25 | } 26 | 27 | /** 28 | * @return array 29 | * @throws Exception 30 | */ 31 | public function getFilter() 32 | { 33 | $filter = $this->getInfoBlockFilter(); 34 | 35 | foreach ($this->where as $entry) { 36 | list($property, $operator, $value) = $entry; 37 | $filter += $this->getFilterRow($property, $operator, $value); 38 | } 39 | 40 | $filter = array_merge($filter, $this->whereRaw); 41 | 42 | return $filter; 43 | } 44 | 45 | /** 46 | * @return array 47 | * @throws InvalidArgumentException 48 | */ 49 | protected function getInfoBlockFilter() 50 | { 51 | $infoBlockType = $this->entityMap->getAnnotation()->getType(); 52 | $infoBlockCode = $this->entityMap->getAnnotation()->getCode(); 53 | $infoBlock = CIBlock::GetList(null, [ 54 | 'TYPE' => $infoBlockType, 55 | 'CODE' => $infoBlockCode, 56 | 'CHECK_PERMISSIONS' => 'N' 57 | ])->Fetch(); 58 | 59 | self::assert(!empty($infoBlock['ID']), "Инфоблок с кодом $infoBlockCode и типом $infoBlockType не найден."); 60 | 61 | return ['=IBLOCK_ID' => $infoBlock['ID']]; 62 | } 63 | 64 | /** 65 | * @param mixed $term 66 | * @param string $msg 67 | */ 68 | protected static function assert($term, $msg) 69 | { 70 | if (!$term) { 71 | throw new InvalidArgumentException($msg); 72 | } 73 | } 74 | 75 | /** 76 | * @param string $property 77 | * @param string $operator 78 | * @param mixed $value 79 | * @return array|null 80 | * @throws InvalidArgumentException 81 | * @throws Exception 82 | */ 83 | protected function getFilterRow($property, $operator, $value) 84 | { 85 | $propertyMap = $this->entityMap->getProperty($property); 86 | $propertyAnnotation = $propertyMap->getAnnotation(); 87 | $type = $propertyAnnotation->getType(); 88 | $code = $propertyAnnotation->getCode(); 89 | 90 | if ($propertyAnnotation instanceof Field) { 91 | return $this->getFieldFilterRow($type, $code, $operator, $value); 92 | } else { 93 | return $this->getPropertyFilterRow($type, $code, $operator, $value); 94 | } 95 | } 96 | 97 | /** 98 | * @param string $type 99 | * @param string $code 100 | * @param string $operator 101 | * @param mixed $value 102 | * @return array 103 | */ 104 | protected static function getFieldFilterRow($type, $code, $operator, $value) 105 | { 106 | $k = $operator . $code; 107 | if ($type === Field::TYPE_BOOLEAN) { 108 | $v = $value && $value !== 'N' ? 'Y' : 'N'; 109 | } else { 110 | $v = $value !== '' && $value !== null ? $value : false; 111 | } 112 | 113 | return [$k => $v]; 114 | } 115 | 116 | /** 117 | * @param string $type 118 | * @param string $code 119 | * @param string $operator 120 | * @param mixed $value 121 | * @return array 122 | * @throws Exception 123 | */ 124 | protected static function getPropertyFilterRow($type, $code, $operator, $value) 125 | { 126 | $k = self::getPropertyFilterKey($type, $code, $operator); 127 | $v = self::getPropertyFilterValue($type, $value); 128 | return [$k => $v]; 129 | } 130 | 131 | /** 132 | * @param string $type 133 | * @param string $code 134 | * @param string $operator 135 | * @return string 136 | */ 137 | protected static function getPropertyFilterKey($type, $code, $operator) 138 | { 139 | if ($type === Property::TYPE_BOOLEAN) { 140 | return "{$operator}PROPERTY_{$code}_VALUE"; 141 | } else { 142 | return "{$operator}PROPERTY_{$code}"; 143 | } 144 | } 145 | 146 | /** 147 | * @param string $type 148 | * @param mixed $value 149 | * @return mixed 150 | * @throws Exception 151 | */ 152 | protected static function getPropertyFilterValue($type, $value) 153 | { 154 | if ($type === Property::TYPE_BOOLEAN) { 155 | return $value && $value !== 'N' ? 'Y' : false; 156 | } 157 | 158 | if ($type === Property::TYPE_DATETIME) { 159 | $dateTime = self::toDateTime($value); 160 | return $dateTime instanceof DateTime ? $dateTime->format('Y-m-d H:i:s') : false; 161 | } 162 | 163 | return ($value === '' || $value === null) ? false : $value; 164 | } 165 | 166 | /** 167 | * @param mixed $value 168 | * @return DateTime|false|null 169 | * @throws Exception 170 | */ 171 | protected static function toDateTime($value) 172 | { 173 | if (!$value) { 174 | return null; 175 | } 176 | 177 | if ($value instanceof DateTime) { 178 | return $value; 179 | } 180 | 181 | if ($value instanceof BitrixDateTime) { 182 | return DateTime::createFromFormat('Y-m-d H:i:s', $value->format('Y-m-d H:i:s')); 183 | } 184 | 185 | return new DateTime($value); 186 | } 187 | } -------------------------------------------------------------------------------- /src/Query/RawResult.php: -------------------------------------------------------------------------------- 1 | id = $id; 31 | $this->infoBlockId = $infoBlockId; 32 | $this->data = $data; 33 | } 34 | 35 | /** 36 | * @return int 37 | */ 38 | public function getId() 39 | { 40 | return $this->id; 41 | } 42 | 43 | /** 44 | * @return int 45 | */ 46 | public function getInfoBlockId() 47 | { 48 | return $this->infoBlockId; 49 | } 50 | 51 | /** 52 | * @return array 53 | */ 54 | public function getData() 55 | { 56 | return $this->data; 57 | } 58 | 59 | /** 60 | * @param string $code 61 | * @return mixed 62 | */ 63 | public function getField($code) 64 | { 65 | if (!array_key_exists($code, $this->data)) { 66 | throw new InvalidArgumentException("Поле $code не найдено в массиве данных."); 67 | } 68 | 69 | return $this->data[$code]; 70 | } 71 | 72 | /** 73 | * @param mixed $term 74 | * @param string $msg 75 | */ 76 | protected static function assert($term, $msg) 77 | { 78 | if (!$term) { 79 | throw new InvalidArgumentException($msg); 80 | } 81 | } 82 | 83 | /** 84 | * @param PropertyMap $property 85 | * @param mixed $rawValue 86 | * @return mixed 87 | * @throws Exception 88 | */ 89 | public static function normalizePropertyValue(PropertyMap $property, $rawValue) 90 | { 91 | if ($property->getAnnotation()->isMultiple()) { 92 | return array_map(function ($value) use ($property) { 93 | return self::normalizeValue($property, $value); 94 | }, is_array($rawValue) ? $rawValue : []); 95 | } else { 96 | return self::normalizeValue($property, $rawValue); 97 | } 98 | } 99 | 100 | /** 101 | * @param PropertyMap $property 102 | * @param mixed $rawValue 103 | * @return mixed 104 | * @throws Exception 105 | */ 106 | public static function normalizeValue(PropertyMap $property, $rawValue) 107 | { 108 | if ($rawValue === null) { 109 | return null; 110 | } 111 | 112 | $type = $property->getAnnotation()->getType(); 113 | 114 | $map = [ 115 | Property::TYPE_ENTITY => function ($value) use ($property) { 116 | $entity = $property->getAnnotation()->getEntity(); 117 | return $value ? Select::from($entity)->whereRaw('ID', $value)->fetch() : null; 118 | }, 119 | Property::TYPE_BOOLEAN => [self::class, 'normalizeBooleanValue'], 120 | Property::TYPE_INTEGER => [self::class, 'normalizeNumericValue'], 121 | Property::TYPE_FLOAT => [self::class, 'normalizeNumericValue'], 122 | Property::TYPE_DATETIME => [self::class, 'normalizeDateTimeValue'], 123 | ]; 124 | 125 | return array_key_exists($type, $map) ? call_user_func($map[$type], $rawValue) : $rawValue; 126 | } 127 | 128 | /** 129 | * @param mixed $value 130 | * @return bool 131 | */ 132 | protected static function normalizeBooleanValue($value) 133 | { 134 | return $value && $value !== 'N' ? true : false; 135 | } 136 | 137 | /** 138 | * @param mixed $value 139 | * @return int|float 140 | */ 141 | protected static function normalizeNumericValue($value) 142 | { 143 | return strstr($value, '.') ? (float)$value : (int)$value; 144 | } 145 | 146 | /** 147 | * @param mixed $value 148 | * @return DateTime 149 | * @throws Exception 150 | */ 151 | protected static function normalizeDateTimeValue($value) 152 | { 153 | return new DateTime($value); 154 | } 155 | } -------------------------------------------------------------------------------- /src/Query/Select.php: -------------------------------------------------------------------------------- 1 | entityMap = EntityMap::fromClass($class); 38 | } 39 | 40 | /** 41 | * @param string $class 42 | * @return Select 43 | * @throws AnnotationException 44 | * @throws ReflectionException 45 | * @throws InvalidArgumentException 46 | */ 47 | public static function from($class) 48 | { 49 | return new self($class); 50 | } 51 | 52 | /** 53 | * Может быть вызвано с двумя или тремя аргументами. 54 | * 55 | * Если 2 аргумента, то название свойства и значение для фильтрации. 56 | * Например: $this->where('name', 'bender'); 57 | * По-умолчанию будет использован оператор сравнения "=". 58 | * 59 | * Если 3 аргумента, то название свойства, оператор сравнения и значение для фильтрации. 60 | * Например: $this->where('age', '>', 18); 61 | * 62 | * @param string $p Название свойства класса для фильтрации. 63 | * @param mixed $_ Если 3 аргумента то оператор сравнения, иначе значение для фильтрации. 64 | * @param mixed Если 3 аргумента то значение для фильтрации. 65 | * @return $this 66 | */ 67 | public function where($p, $_) 68 | { 69 | if (func_num_args() > 2) { 70 | $property = $p; 71 | $operator = $_; 72 | $value = func_get_arg(2); 73 | } else { 74 | $property = $p; 75 | $operator = '='; 76 | $value = $_; 77 | } 78 | 79 | $this->where[] = [$property, $operator, $value]; 80 | return $this; 81 | } 82 | 83 | /** 84 | * @param string $f 85 | * @param mixed $_ 86 | * @param mixed 87 | * @return $this 88 | */ 89 | public function whereRaw($f, $_) 90 | { 91 | if (func_num_args() > 2) { 92 | $field = $f; 93 | $operator = $_; 94 | $value = func_get_arg(2); 95 | } else { 96 | $field = $f; 97 | $operator = '='; 98 | $value = $_; 99 | } 100 | 101 | $this->whereRaw[$operator . $field] = $value; 102 | return $this; 103 | } 104 | 105 | /** 106 | * @param string $p 107 | * @param string $d 108 | * @return $this 109 | */ 110 | public function orderBy($p, $d = 'asc') 111 | { 112 | $this->orderBy[$p] = $d; 113 | return $this; 114 | } 115 | 116 | /** 117 | * @return Generator|RawResult[] 118 | * @throws InvalidArgumentException 119 | * @throws Exception 120 | */ 121 | public function rawIterator() 122 | { 123 | $filterBuilder = new FilterBuilder($this->entityMap, $this->where, $this->whereRaw); 124 | 125 | $filter = $filterBuilder->getFilter(); 126 | $order = $this->getOrderingRules(); 127 | 128 | $rs = CIBlockElement::GetList($order, $filter); 129 | while ($element = $rs->GetNextElement()) { 130 | if ($element instanceof _CIBElement) { 131 | $data = array_merge($this->getFieldsData($element), $this->getPropertiesData($element)); 132 | $elementFields = $element->GetFields(); 133 | yield new RawResult($elementFields['ID'], $elementFields['IBLOCK_ID'], $data); 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * @return Generator 140 | * @throws ReflectionException 141 | * @throws InvalidArgumentException 142 | * @throws Exception 143 | */ 144 | public function iterator() 145 | { 146 | $classRef = new ReflectionClass($this->entityMap->getClass()); 147 | foreach ($this->rawIterator() as $rawResult) { 148 | $object = $classRef->newInstanceWithoutConstructor(); 149 | yield self::hydrate($object, $rawResult->getData()); 150 | } 151 | } 152 | 153 | /** 154 | * @return object 155 | * @throws ReflectionException 156 | * @throws InvalidArgumentException 157 | * @throws Exception 158 | */ 159 | public function fetch() 160 | { 161 | if ($this->generator instanceof Generator) { 162 | $this->generator->next(); 163 | } else { 164 | $this->generator = $this->iterator(); 165 | } 166 | 167 | return $this->generator->current(); 168 | } 169 | 170 | /** 171 | * @return object[] 172 | * @throws ReflectionException 173 | * @throws InvalidArgumentException 174 | * @throws Exception 175 | */ 176 | public function fetchAll() 177 | { 178 | $array = []; 179 | foreach ($this->iterator() as $object) { 180 | $array[] = $object; 181 | } 182 | 183 | return $array; 184 | } 185 | 186 | /** 187 | * @param mixed $term 188 | * @param string $msg 189 | */ 190 | protected static function assert($term, $msg) 191 | { 192 | if (!$term) { 193 | throw new InvalidArgumentException($msg); 194 | } 195 | } 196 | 197 | /** 198 | * @return array 199 | */ 200 | protected function getOrderingRules() 201 | { 202 | $order = []; 203 | foreach ($this->orderBy as $property => $direction) { 204 | $propertyAnnotation = $this->entityMap->getProperty($property)->getAnnotation(); 205 | if ($propertyAnnotation instanceof Field) { 206 | $order[$propertyAnnotation->getCode()] = $direction; 207 | } elseif ($propertyAnnotation instanceof Property) { 208 | $order['PROPERTY_' . $propertyAnnotation->getCode()] = $direction; 209 | } 210 | } 211 | 212 | return $order; 213 | } 214 | 215 | /** 216 | * @param _CIBElement $element 217 | * @return array 218 | * @throws Exception 219 | */ 220 | protected function getFieldsData(_CIBElement $element) 221 | { 222 | $fields = array_filter($this->entityMap->getProperties(), function (PropertyMap $propertyMap) { 223 | return $propertyMap->getAnnotation() instanceof Field; 224 | }); 225 | 226 | $data = []; 227 | $elementFields = $element->GetFields(); 228 | foreach ($fields as $field) { 229 | $key = $field->getAnnotation()->getCode(); 230 | self::assert( 231 | array_key_exists($key, $elementFields), 232 | "Поле $key не найдено в результатах CIBlockElement::GetList()." 233 | ); 234 | 235 | $data[$field->getCode()] = RawResult::normalizeValue($field, $elementFields[$key]); 236 | } 237 | 238 | return $data; 239 | } 240 | 241 | /** 242 | * @param _CIBElement $element 243 | * @return array 244 | * @throws Exception 245 | */ 246 | protected function getPropertiesData(_CIBElement $element) 247 | { 248 | $properties = array_filter($this->entityMap->getProperties(), function (PropertyMap $propertyMap) { 249 | return $propertyMap->getAnnotation() instanceof Property; 250 | }); 251 | 252 | $data = []; 253 | $elementProperties = $element->GetProperties(); 254 | foreach ($properties as $property) { 255 | $key = $property->getAnnotation()->getCode(); 256 | self::assert( 257 | array_key_exists($key, $elementProperties) && array_key_exists('VALUE', $elementProperties[$key]), 258 | "Свойство $key не найдено в результатах CIBlockElement::GetList()." 259 | ); 260 | 261 | $rawValue = $elementProperties[$key]['VALUE']; 262 | $data[$property->getCode()] = RawResult::normalizePropertyValue($property, $rawValue); 263 | } 264 | 265 | return $data; 266 | } 267 | 268 | /** 269 | * @param object $object 270 | * @param array $data 271 | * @return object 272 | * @throws ReflectionException 273 | * @throws InvalidArgumentException 274 | */ 275 | protected static function hydrate($object, array $data) 276 | { 277 | self::assert(is_object($object), 'Аргумент $object не является объектом.'); 278 | $objectRef = new ReflectionObject($object); 279 | foreach ($data as $key => $value) { 280 | $propRef = $objectRef->getProperty($key); 281 | if (!$propRef->isPublic()) { 282 | $propRef->setAccessible(true); 283 | $propRef->setValue($object, $value); 284 | $propRef->setAccessible(false); 285 | } else { 286 | $propRef->setValue($object, $value); 287 | } 288 | } 289 | 290 | return $object; 291 | } 292 | 293 | /** 294 | * Вернуть текущий объект. 295 | * 296 | * @return object 297 | * @throws ReflectionException 298 | */ 299 | public function current() 300 | { 301 | $this->generator = isset($this->generator) ? $this->generator : $this->iterator(); 302 | return $this->generator->current(); 303 | } 304 | 305 | /** 306 | * Переместить курсор к следующему объекту. 307 | * 308 | * @throws ReflectionException 309 | */ 310 | public function next() 311 | { 312 | $this->generator = isset($this->generator) ? $this->generator : $this->iterator(); 313 | $this->generator->next(); 314 | } 315 | 316 | /** 317 | * Вернуть индекс текущего объекта. 318 | * 319 | * @return mixed 320 | * @throws ReflectionException 321 | */ 322 | public function key() 323 | { 324 | $this->generator = isset($this->generator) ? $this->generator : $this->iterator(); 325 | return $this->generator->key(); 326 | } 327 | 328 | /** 329 | * Проверить текущую позицию курсора. 330 | * 331 | * @return bool 332 | * @throws ReflectionException 333 | */ 334 | public function valid() 335 | { 336 | $this->generator = isset($this->generator) ? $this->generator : $this->iterator(); 337 | return $this->generator->valid(); 338 | } 339 | 340 | /** 341 | * Начать новую итерацию. 342 | * 343 | * @throws ReflectionException 344 | */ 345 | public function rewind() 346 | { 347 | $this->generator = $this->iterator(); 348 | } 349 | } -------------------------------------------------------------------------------- /src/SchemaBuilder.php: -------------------------------------------------------------------------------- 1 | getAnnotation()); 23 | foreach ($entityMap->getProperties() as $propertyMap) { 24 | $propAnnotation = $propertyMap->getAnnotation(); 25 | if ($propAnnotation instanceof Property) { 26 | self::buildProperty($entityMap->getAnnotation(), $propAnnotation); 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | 33 | /** 34 | * @param InfoBlock $annotation 35 | * @return int 36 | * @throws InvalidArgumentException 37 | */ 38 | protected static function buildInfoBlock(InfoBlock $annotation) 39 | { 40 | $type = $annotation->getType(); 41 | self::assert($type, 'Не указан тип инфоблока.'); 42 | $code = $annotation->getCode(); 43 | self::assert($code, 'Не указан код инфоблока.'); 44 | $name = $annotation->getName(); 45 | 46 | $fields = [ 47 | 'LID' => self::getSiteCodes(), 48 | 'CODE' => $code, 49 | 'IBLOCK_TYPE_ID' => $type, 50 | 'NAME' => $name ? $name : $code, 51 | 'GROUP_ID' => ['1' => 'X', '2' => 'W'] 52 | ]; 53 | 54 | $exist = self::getBitrixInfoBlock($type, $code); 55 | if (!empty($exist['ID'])) { 56 | $iBlockId = $exist['ID']; 57 | $cIBlock = new CIBlock(); 58 | $isUpdated = $cIBlock->Update($iBlockId, $fields); 59 | self::assert($isUpdated, strip_tags($cIBlock->LAST_ERROR)); 60 | } else { 61 | $cIBlock = new CIBlock(); 62 | /** @var int|bool $iBlockId */ 63 | $iBlockId = $cIBlock->Add($fields); 64 | self::assert($iBlockId, strip_tags($cIBlock->LAST_ERROR)); 65 | } 66 | 67 | return $iBlockId; 68 | } 69 | 70 | /** 71 | * @return string[] 72 | */ 73 | protected static function getSiteCodes() 74 | { 75 | $siteCodes = []; 76 | $rs = CSite::GetList($by, $order); 77 | while ($site = $rs->Fetch()) { 78 | $siteCodes[] = $site['LID']; 79 | } 80 | 81 | return $siteCodes; 82 | } 83 | 84 | /** 85 | * @param mixed $term 86 | * @param string $msg 87 | * @throws InvalidArgumentException 88 | */ 89 | protected static function assert($term, $msg) 90 | { 91 | if (!$term) { 92 | throw new InvalidArgumentException($msg); 93 | } 94 | } 95 | 96 | /** 97 | * @param string $type 98 | * @param string $code 99 | * @return array 100 | * @throws InvalidArgumentException 101 | */ 102 | protected static function getBitrixInfoBlock($type, $code) 103 | { 104 | self::assert($type, 'Не указан тип инфоблока.'); 105 | self::assert($code, 'Не указан код инфоблока.'); 106 | 107 | $iBlock = CIBlock::GetList(null, [ 108 | '=TYPE' => $type, 109 | '=CODE' => $code, 110 | 'CHECK_PERMISSIONS' => 'N' 111 | ])->Fetch(); 112 | 113 | return $iBlock; 114 | } 115 | 116 | /** 117 | * @param int $iBlockId 118 | * @param string $code 119 | * @return array|null 120 | * @throws InvalidArgumentException 121 | */ 122 | protected static function getBitrixProperty($iBlockId, $code) 123 | { 124 | self::assert($iBlockId, 'Не указан ID инфоблока.'); 125 | self::assert($code, 'Не указан код свойства.'); 126 | 127 | $prop = CIBlockProperty::GetList(null, [ 128 | 'IBLOCK_ID' => $iBlockId, 129 | 'CODE' => $code, 130 | 'CHECK_PERMISSIONS' => 'N' 131 | ])->Fetch(); 132 | 133 | return $prop; 134 | } 135 | 136 | protected static $propertyMap = [ 137 | Property::TYPE_INTEGER => ['PROPERTY_TYPE' => 'N', 'USER_TYPE' => false], 138 | Property::TYPE_FLOAT => ['PROPERTY_TYPE' => 'N', 'USER_TYPE' => false], 139 | Property::TYPE_DATETIME => ['PROPERTY_TYPE' => 'S', 'USER_TYPE' => 'DateTime'], 140 | Property::TYPE_FILE => ['PROPERTY_TYPE' => 'F', 'USER_TYPE' => false], 141 | Property::TYPE_BOOLEAN => ['PROPERTY_TYPE' => 'L', 'LIST_TYPE' => 'C', 'USER_TYPE' => false], 142 | Property::TYPE_ENTITY => ['PROPERTY_TYPE' => 'E', 'USER_TYPE' => false], 143 | Property::TYPE_STRING => ['PROPERTY_TYPE' => 'S', 'USER_TYPE' => false], 144 | ]; 145 | 146 | /** 147 | * @param InfoBlock $entityAnnotation 148 | * @param Property $propertyAnnotation 149 | * @return bool 150 | * @throws InvalidArgumentException 151 | */ 152 | protected static function buildProperty(InfoBlock $entityAnnotation, Property $propertyAnnotation) 153 | { 154 | self::assert($propertyAnnotation->getCode(), 'Не указан код свойства.'); 155 | self::assert($propertyAnnotation->getType(), 'Не указан тип свойства.'); 156 | 157 | $iBlock = self::getBitrixInfoBlock($entityAnnotation->getType(), $entityAnnotation->getCode()); 158 | self::assert( 159 | !empty($iBlock['ID']), 160 | "Инфоблок с кодом {$entityAnnotation->getCode()} и типом {$entityAnnotation->getType()} не найден." 161 | ); 162 | 163 | $fields = self::generateBitrixFields($iBlock['ID'], $propertyAnnotation); 164 | 165 | $exist = self::getBitrixProperty($iBlock['ID'], $propertyAnnotation->getCode()); 166 | if (!empty($exist['ID'])) { 167 | $propId = $exist['ID']; 168 | self::updateProperty($propId, $propertyAnnotation, $fields); 169 | } else { 170 | $propId = self::addProperty($propertyAnnotation, $fields); 171 | } 172 | 173 | return $propId; 174 | } 175 | 176 | /** 177 | * @param int $iBlockId 178 | * @param Property $propertyAnnotation 179 | * @return array 180 | * @throws InvalidArgumentException 181 | */ 182 | protected static function generateBitrixFields($iBlockId, Property $propertyAnnotation) 183 | { 184 | $fields = [ 185 | 'IBLOCK_ID' => $iBlockId, 186 | 'CODE' => $propertyAnnotation->getCode(), 187 | 'NAME' => $propertyAnnotation->getName() ? $propertyAnnotation->getName() : $propertyAnnotation->getCode(), 188 | 'MULTIPLE' => $propertyAnnotation->isMultiple() ? 'Y' : 'N', 189 | 'FILTRABLE' => 'Y' 190 | ]; 191 | 192 | if (array_key_exists($propertyAnnotation->getType(), self::$propertyMap)) { 193 | $fields += self::$propertyMap[$propertyAnnotation->getType()]; 194 | } 195 | 196 | return $fields; 197 | } 198 | 199 | /** 200 | * @param int $propId 201 | * @param Property $propertyAnnotation 202 | * @param array $fields 203 | * @return bool 204 | * @throws InvalidArgumentException 205 | */ 206 | protected static function updateProperty($propId, Property $propertyAnnotation, array $fields) 207 | { 208 | $type = $propertyAnnotation->getType(); 209 | if ($type === Property::TYPE_BOOLEAN) { 210 | $existEnum = CIBlockProperty::GetPropertyEnum($propId, null, ['XML_ID' => 'Y', 'VALUE' => 'Y'])->Fetch(); 211 | if (!$existEnum) { 212 | $fields += ['VALUES' => [['XML_ID' => 'Y', 'VALUE' => 'Y', 'DEF' => 'N']]]; 213 | } 214 | } 215 | 216 | $cIBlockProperty = new CIBlockProperty(); 217 | $isUpdated = $cIBlockProperty->Update($propId, $fields); 218 | self::assert($isUpdated, strip_tags($cIBlockProperty->LAST_ERROR)); 219 | 220 | return $isUpdated; 221 | } 222 | 223 | /** 224 | * @param Property $propertyAnnotation 225 | * @param array $fields 226 | * @return int 227 | * @throws InvalidArgumentException 228 | */ 229 | protected static function addProperty(Property $propertyAnnotation, array $fields) 230 | { 231 | $type = $propertyAnnotation->getType(); 232 | if ($type === Property::TYPE_BOOLEAN) { 233 | $fields += ['VALUES' => [['XML_ID' => 'Y', 'VALUE' => 'Y', 'DEF' => 'N']]]; 234 | } 235 | 236 | $cIBlockProperty = new CIBlockProperty(); 237 | $propId = $cIBlockProperty->Add($fields); 238 | self::assert($propId, strip_tags($cIBlockProperty->LAST_ERROR)); 239 | 240 | return $propId; 241 | } 242 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | setName($name); 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getId() 37 | { 38 | return $this->id; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getName() 45 | { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * @param string $name 51 | */ 52 | public function setName($name) 53 | { 54 | $this->name = $name; 55 | } 56 | } -------------------------------------------------------------------------------- /tests/resources/Entity/Book.php: -------------------------------------------------------------------------------- 1 | id; 97 | } 98 | 99 | /** 100 | * @param string $imgPath 101 | */ 102 | public function setCover($imgPath) 103 | { 104 | if (empty($imgPath)) { 105 | $this->cover = null; 106 | return; 107 | } 108 | 109 | if (!is_file($imgPath)) { 110 | throw new InvalidArgumentException("Файл $imgPath не найден."); 111 | } 112 | 113 | $content = file_get_contents($imgPath); 114 | $imgInfo = getimagesizefromstring($content); 115 | 116 | if (empty($imgInfo['mime']) || !preg_match('/^image\//ui', $imgInfo['mime'])) { 117 | throw new InvalidArgumentException("Файл $imgPath не является файлом изображения."); 118 | } 119 | 120 | $arFile = [ 121 | 'name' => pathinfo($imgPath, PATHINFO_BASENAME), 122 | 'type' => $imgInfo['mime'], 123 | 'content' => $content 124 | ]; 125 | 126 | $fileId = CFile::SaveFile($arFile, 'books'); 127 | if (!$fileId) { 128 | throw new RuntimeException("Ошибка сохранения файла $imgPath."); 129 | } 130 | 131 | $this->cover = $fileId; 132 | } 133 | 134 | /** 135 | * @return string|null 136 | */ 137 | public function getCover() 138 | { 139 | return $this->cover ? CFile::GetPath($this->cover) : null; 140 | } 141 | } -------------------------------------------------------------------------------- /tests/resources/Entity/WithConflictPropertyAnnotations.php: -------------------------------------------------------------------------------- 1 | assertTrue(CModule::IncludeModule('iblock'), "Can't load iblock module."); 13 | } 14 | } -------------------------------------------------------------------------------- /tests/src/FunctionalTest/EntityMapTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(EntityMap::class, $entityMap); 29 | $this->assertInstanceOf(InfoBlock::class, $entityMap->getAnnotation()); 30 | $this->assertInstanceOf(ReflectionClass::class, $entityMap->getReflection()); 31 | $this->assertContainsOnlyInstancesOf(PropertyMap::class, $entityMap->getProperties()); 32 | 33 | foreach ($entityMap->getProperties() as $propertyMap) { 34 | $this->assertInstanceOf(PropertyAnnotationInterface::class, $propertyMap->getAnnotation()); 35 | $this->assertInstanceOf(ReflectionProperty::class, $propertyMap->getReflection()); 36 | } 37 | 38 | return $entityMap; 39 | } 40 | 41 | /** 42 | * @depends testCanBuildEntityMap 43 | * @param EntityMap $entityMap 44 | * @return EntityMap 45 | */ 46 | public function testIsEntityMapCorrect(EntityMap $entityMap) 47 | { 48 | $this->assertEquals('Entity\Book', $entityMap->getClass()); 49 | $this->assertEquals('test_entity', $entityMap->getAnnotation()->getType()); 50 | $this->assertEquals('books', $entityMap->getAnnotation()->getCode()); 51 | $this->assertEquals('Книги', $entityMap->getAnnotation()->getName()); 52 | 53 | $this->assertEquals('title', $entityMap->getProperty('title')->getCode()); 54 | $this->assertInstanceOf(Field::class, $entityMap->getProperty('title')->getAnnotation()); 55 | $this->assertEquals('NAME', $entityMap->getProperty('title')->getAnnotation()->getCode()); 56 | $this->assertEquals(Field::TYPE_STRING, $entityMap->getProperty('title')->getAnnotation()->getType()); 57 | $this->assertEquals(false, $entityMap->getProperty('title')->getAnnotation()->isPrimaryKey()); 58 | $this->assertEmpty($entityMap->getProperty('title')->getAnnotation()->isMultiple()); 59 | 60 | $this->assertEquals('isShow', $entityMap->getProperty('isShow')->getCode()); 61 | $this->assertInstanceOf(Field::class, $entityMap->getProperty('isShow')->getAnnotation()); 62 | $this->assertEquals('ACTIVE', $entityMap->getProperty('isShow')->getAnnotation()->getCode()); 63 | $this->assertEquals(Field::TYPE_BOOLEAN, $entityMap->getProperty('isShow')->getAnnotation()->getType()); 64 | $this->assertEquals(false, $entityMap->getProperty('isShow')->getAnnotation()->isPrimaryKey()); 65 | $this->assertEmpty($entityMap->getProperty('isShow')->getAnnotation()->isMultiple()); 66 | 67 | $this->assertEquals('coAuthors', $entityMap->getProperty('coAuthors')->getCode()); 68 | $this->assertInstanceOf(Property::class, $entityMap->getProperty('coAuthors')->getAnnotation()); 69 | $this->assertEquals('co_authors', $entityMap->getProperty('coAuthors')->getAnnotation()->getCode()); 70 | $this->assertEquals(Property::TYPE_ENTITY, $entityMap->getProperty('coAuthors')->getAnnotation()->getType()); 71 | $this->assertEquals(false, $entityMap->getProperty('coAuthors')->getAnnotation()->isPrimaryKey()); 72 | $this->assertEquals('Соавторы', $entityMap->getProperty('coAuthors')->getAnnotation()->getName()); 73 | $this->assertTrue($entityMap->getProperty('coAuthors')->getAnnotation()->isMultiple()); 74 | 75 | $this->assertEquals('publishedAt', $entityMap->getProperty('publishedAt')->getCode()); 76 | $this->assertInstanceOf(Property::class, $entityMap->getProperty('publishedAt')->getAnnotation()); 77 | $this->assertEquals('published_at', $entityMap->getProperty('publishedAt')->getAnnotation()->getCode()); 78 | $this->assertEquals(Property::TYPE_DATETIME, $entityMap->getProperty('publishedAt')->getAnnotation()->getType()); 79 | $this->assertEquals(false, $entityMap->getProperty('publishedAt')->getAnnotation()->isPrimaryKey()); 80 | $this->assertEquals('Опубликована', $entityMap->getProperty('publishedAt')->getAnnotation()->getName()); 81 | $this->assertEmpty($entityMap->getProperty('publishedAt')->getAnnotation()->isMultiple()); 82 | 83 | $this->assertEquals('isBestseller', $entityMap->getProperty('isBestseller')->getCode()); 84 | $this->assertInstanceOf(Property::class, $entityMap->getProperty('isBestseller')->getAnnotation()); 85 | $this->assertEquals('is_bestseller', $entityMap->getProperty('isBestseller')->getAnnotation()->getCode()); 86 | $this->assertEquals(Property::TYPE_BOOLEAN, $entityMap->getProperty('isBestseller')->getAnnotation()->getType()); 87 | $this->assertEquals(false, $entityMap->getProperty('isBestseller')->getAnnotation()->isPrimaryKey()); 88 | $this->assertEquals('Бестселлер', $entityMap->getProperty('isBestseller')->getAnnotation()->getName()); 89 | $this->assertEmpty($entityMap->getProperty('isBestseller')->getAnnotation()->isMultiple()); 90 | 91 | $this->assertEquals('pagesNum', $entityMap->getProperty('pagesNum')->getCode()); 92 | $this->assertInstanceOf(Property::class, $entityMap->getProperty('pagesNum')->getAnnotation()); 93 | $this->assertEquals('pages_num', $entityMap->getProperty('pagesNum')->getAnnotation()->getCode()); 94 | $this->assertEquals(Property::TYPE_INTEGER, $entityMap->getProperty('pagesNum')->getAnnotation()->getType()); 95 | $this->assertEquals(false, $entityMap->getProperty('pagesNum')->getAnnotation()->isPrimaryKey()); 96 | $this->assertEquals('Кол-во страниц', $entityMap->getProperty('pagesNum')->getAnnotation()->getName()); 97 | $this->assertEmpty($entityMap->getProperty('pagesNum')->getAnnotation()->isMultiple()); 98 | 99 | $this->assertEquals('tags', $entityMap->getProperty('tags')->getCode()); 100 | $this->assertInstanceOf(Property::class, $entityMap->getProperty('tags')->getAnnotation()); 101 | $this->assertEquals('tags', $entityMap->getProperty('tags')->getAnnotation()->getCode()); 102 | $this->assertEquals(Property::TYPE_STRING, $entityMap->getProperty('tags')->getAnnotation()->getType()); 103 | $this->assertEquals(false, $entityMap->getProperty('tags')->getAnnotation()->isPrimaryKey()); 104 | $this->assertEquals('Теги', $entityMap->getProperty('tags')->getAnnotation()->getName()); 105 | $this->assertTrue($entityMap->getProperty('tags')->getAnnotation()->isMultiple()); 106 | 107 | $this->assertEquals('republicationsAt', $entityMap->getProperty('republicationsAt')->getCode()); 108 | $this->assertInstanceOf(Property::class, $entityMap->getProperty('republicationsAt')->getAnnotation()); 109 | $this->assertEquals('republications_at', $entityMap->getProperty('republicationsAt')->getAnnotation()->getCode()); 110 | $this->assertEquals(Property::TYPE_DATETIME, $entityMap->getProperty('republicationsAt')->getAnnotation()->getType()); 111 | $this->assertEquals(false, $entityMap->getProperty('republicationsAt')->getAnnotation()->isPrimaryKey()); 112 | $this->assertEquals('Переиздания', $entityMap->getProperty('republicationsAt')->getAnnotation()->getName()); 113 | $this->assertTrue($entityMap->getProperty('republicationsAt')->getAnnotation()->isMultiple()); 114 | 115 | $this->assertEquals('id', $entityMap->getProperty('id')->getCode()); 116 | $this->assertInstanceOf(Field::class, $entityMap->getProperty('id')->getAnnotation()); 117 | $this->assertEquals('ID', $entityMap->getProperty('id')->getAnnotation()->getCode()); 118 | $this->assertEquals(Field::TYPE_INTEGER, $entityMap->getProperty('id')->getAnnotation()->getType()); 119 | $this->assertEquals(true, $entityMap->getProperty('id')->getAnnotation()->isPrimaryKey()); 120 | $this->assertEmpty($entityMap->getProperty('id')->getAnnotation()->isMultiple()); 121 | 122 | return $entityMap; 123 | } 124 | } -------------------------------------------------------------------------------- /tests/src/FunctionalTest/EntityMapperTest.php: -------------------------------------------------------------------------------- 1 | title = 'Остров сокровищ'; 55 | $book->isShow = true; 56 | $book->author = new Author('Р. Л. Стивенсон'); 57 | $book->coAuthors[] = new Author('Неизвестный автор'); 58 | $book->coAuthors[] = new Author('Неизвестный автор 2'); 59 | $book->isBestseller = true; 60 | $book->pagesNum = 350; 61 | $book->tags = ['приключения', 'пираты']; 62 | $book->publishedAt = DateTime::createFromFormat('d.m.Y H:i:s', '14.06.1883 00:00:00'); 63 | $book->republicationsAt = [ 64 | DateTime::createFromFormat('d.m.Y H:i:s', '01.09.1901 00:00:00'), 65 | DateTime::createFromFormat('d.m.Y H:i:s', '07.05.2001 00:00:00') 66 | ]; 67 | 68 | $book->setCover(__DIR__ . '/../../resources/cover.jpg'); 69 | $bookRef = new ReflectionObject($book); 70 | $coverRef = $bookRef->getProperty('cover'); 71 | $coverRef->setAccessible(true); 72 | $coverFileId = $coverRef->getValue($book); 73 | $coverRef->setAccessible(false); 74 | $this->assertNotEmpty($coverFileId); 75 | 76 | $id = EntityMapper::save($book); 77 | $this->assertNotEmpty($id); 78 | 79 | return [ 80 | 'id' => $id, 81 | 'coverFileId' => $coverFileId 82 | ]; 83 | } 84 | 85 | /** 86 | * @depends testCanSaveNewObject 87 | * @param array $stack 88 | * @return array 89 | * @throws Exception 90 | */ 91 | public function testIsSavedCorrect(array $stack) 92 | { 93 | $id = $stack['id']; 94 | $coverFileId = $stack['coverFileId']; 95 | 96 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 97 | $this->assertInstanceOf(_CIBElement::class, $element); 98 | 99 | $fields = $element->GetFields(); 100 | $this->assertEquals($id, $fields['ID']); 101 | $this->assertEquals('Остров сокровищ', $fields['NAME']); 102 | $this->assertEquals('Y', $fields['ACTIVE']); 103 | 104 | $properties = $element->GetProperties(); 105 | $this->assertEquals('Y', $properties['is_bestseller']['VALUE']); 106 | $this->assertEquals(350, $properties['pages_num']['VALUE']); 107 | $this->assertEquals(['приключения', 'пираты'], $properties['tags']['VALUE']); 108 | $this->assertEquals($coverFileId, $properties['cover']['VALUE']); 109 | 110 | $this->assertNotEmpty($properties['author']['VALUE']); 111 | $bitrixAuthor = CIBlockElement::GetList(null, ['ID' => $properties['author']['VALUE']])->Fetch(); 112 | $this->assertNotEmpty($bitrixAuthor['NAME']); 113 | $this->assertEquals('Р. Л. Стивенсон', $bitrixAuthor['NAME']); 114 | 115 | $this->assertNotEmpty($properties['co_authors']['VALUE']); 116 | $this->assertTrue(is_array($properties['co_authors']['VALUE'])); 117 | 118 | $coAuthorNames = []; 119 | $childElementRs = CIBlockElement::GetList(null, ['ID' => $properties['co_authors']['VALUE']]); 120 | while ($childElement = $childElementRs->Fetch()) { 121 | $coAuthorNames[] = $childElement['NAME']; 122 | } 123 | 124 | $this->assertEmpty(array_diff(['Неизвестный автор', 'Неизвестный автор 2'], $coAuthorNames)); 125 | $this->assertEmpty(array_diff($coAuthorNames, ['Неизвестный автор', 'Неизвестный автор 2'])); 126 | 127 | $this->assertEquals( 128 | DateTime::createFromFormat('d.m.Y H:i:s', '14.06.1883 00:00:00')->getTimestamp(), 129 | (new DateTime($properties['published_at']['VALUE']))->getTimestamp() 130 | ); 131 | 132 | $this->assertEquals( 133 | [ 134 | DateTime::createFromFormat('d.m.Y H:i:s', '01.09.1901 00:00:00')->getTimestamp(), 135 | DateTime::createFromFormat('d.m.Y H:i:s', '07.05.2001 00:00:00')->getTimestamp() 136 | ], 137 | array_map(function ($strDate) { 138 | return (new DateTime($strDate))->getTimestamp(); 139 | }, $properties['republications_at']['VALUE']) 140 | ); 141 | 142 | return $stack; 143 | } 144 | 145 | /** 146 | * @depends testIsSavedCorrect 147 | * @param array $stack 148 | * @return array 149 | * @throws AnnotationException 150 | * @throws ReflectionException 151 | */ 152 | public function testCanUpdateObject(array $stack) 153 | { 154 | $id = $stack['id']; 155 | 156 | /** @var Book $book */ 157 | $book = EntityMapper::select(Book::class)->where('id', $id)->fetch(); 158 | $this->assertInstanceOf(Book::class, $book); 159 | 160 | /** @var Author $author */ 161 | $author = EntityMapper::select(Author::class)->where('name', 'Р. Л. Стивенсон')->fetch(); 162 | $this->assertInstanceOf(Author::class, $author); 163 | $this->assertEquals('Р. Л. Стивенсон', $author->getName()); 164 | $author->setName('Т. Пратчетт'); 165 | 166 | $book->title = 'Цвет волшебства'; 167 | $book->isShow = false; 168 | $book->author = $author; 169 | $book->coAuthors = []; 170 | $book->isBestseller = false; 171 | $book->pagesNum = 300; 172 | $book->tags = ['приключения', 'фентези']; 173 | $book->publishedAt = DateTime::createFromFormat('d.m.Y H:i:s', '01.09.1983 00:00:00'); 174 | $book->republicationsAt = [ 175 | DateTime::createFromFormat('d.m.Y H:i:s', '12.06.1991 00:00:00'), 176 | DateTime::createFromFormat('d.m.Y H:i:s', '31.12.2007 00:00:00') 177 | ]; 178 | 179 | $updatedId = EntityMapper::save($book); 180 | $this->assertNotEmpty($updatedId); 181 | $this->assertEquals($id, $updatedId); 182 | 183 | return $stack; 184 | } 185 | 186 | /** 187 | * @depends testCanUpdateObject 188 | * @param array $stack 189 | * @return array 190 | * @throws Exception 191 | */ 192 | public function testIsUpdatedCorrect(array $stack) 193 | { 194 | $id = $stack['id']; 195 | $coverFileId = $stack['coverFileId']; 196 | 197 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 198 | $this->assertInstanceOf(_CIBElement::class, $element); 199 | 200 | $fields = $element->GetFields(); 201 | $this->assertEquals($id, $fields['ID']); 202 | $this->assertEquals('Цвет волшебства', $fields['NAME']); 203 | $this->assertEquals('N', $fields['ACTIVE']); 204 | 205 | $properties = $element->GetProperties(); 206 | $this->assertEquals(false, $properties['is_bestseller']['VALUE']); 207 | $this->assertEquals(300, $properties['pages_num']['VALUE']); 208 | $this->assertEquals(['приключения', 'фентези'], $properties['tags']['VALUE']); 209 | $this->assertEquals($coverFileId, $properties['cover']['VALUE']); 210 | 211 | $this->assertNotEmpty($properties['author']['VALUE']); 212 | $bitrixAuthor = CIBlockElement::GetList(null, ['ID' => $properties['author']['VALUE']])->Fetch(); 213 | $this->assertNotEmpty($bitrixAuthor['NAME']); 214 | $this->assertEquals('Т. Пратчетт', $bitrixAuthor['NAME']); 215 | 216 | $this->assertEmpty($properties['co_authors']['VALUE']); 217 | 218 | $this->assertEquals( 219 | DateTime::createFromFormat('d.m.Y H:i:s', '01.09.1983 00:00:00')->getTimestamp(), 220 | (new DateTime($properties['published_at']['VALUE']))->getTimestamp() 221 | ); 222 | 223 | $this->assertEquals( 224 | [ 225 | DateTime::createFromFormat('d.m.Y H:i:s', '12.06.1991 00:00:00')->getTimestamp(), 226 | DateTime::createFromFormat('d.m.Y H:i:s', '31.12.2007 00:00:00')->getTimestamp() 227 | ], 228 | array_map(function ($strDate) { 229 | return (new DateTime($strDate))->getTimestamp(); 230 | }, $properties['republications_at']['VALUE']) 231 | ); 232 | 233 | return $stack; 234 | } 235 | 236 | /** 237 | * @depends testIsUpdatedCorrect 238 | * @param array $stack 239 | * @return array 240 | * @throws AnnotationException 241 | * @throws ReflectionException 242 | */ 243 | public function testCanSaveEmptyChildEntities(array $stack) 244 | { 245 | $id = $stack['id']; 246 | 247 | /** @var Book $book */ 248 | $book = EntityMapper::select(Book::class)->where('id', $id)->fetch(); 249 | $this->assertInstanceOf(Book::class, $book); 250 | 251 | $book->author = null; 252 | $book->coAuthors = null; 253 | 254 | $updatedId = EntityMapper::save($book); 255 | $this->assertNotEmpty($updatedId); 256 | $this->assertEquals($id, $updatedId); 257 | 258 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 259 | $this->assertInstanceOf(_CIBElement::class, $element); 260 | 261 | $fields = $element->GetFields(); 262 | $this->assertEquals($id, $fields['ID']); 263 | 264 | $properties = $element->GetProperties(); 265 | $this->assertEmpty($properties['author']['VALUE']); 266 | $this->assertEmpty($properties['co_authors']['VALUE']); 267 | 268 | return $stack; 269 | } 270 | 271 | /** 272 | * @depends testIsUpdatedCorrect 273 | * @param array $stack 274 | * @return array 275 | * @throws AnnotationException 276 | * @throws ReflectionException 277 | */ 278 | public function testCanSkipUnmodifiedObjectSave(array $stack) 279 | { 280 | $id = $stack['id']; 281 | 282 | /** @var Book $book */ 283 | $book = EntityMapper::select(Book::class)->where('id', $id)->fetch(); 284 | $this->assertInstanceOf(Book::class, $book); 285 | 286 | $element = CIBlockElement::GetList(null, ['ID' => $id])->Fetch(); 287 | $this->assertNotEmpty($element['TIMESTAMP_X']); 288 | $oldTimestamp = $element['TIMESTAMP_X']; 289 | sleep(2); 290 | 291 | $updatedId = EntityMapper::save($book); 292 | $this->assertNotEmpty($updatedId); 293 | $this->assertEquals($id, $updatedId); 294 | 295 | $element = CIBlockElement::GetByID($id)->Fetch(); 296 | $this->assertNotEmpty($element['TIMESTAMP_X']); 297 | $this->assertEquals($oldTimestamp, $element['TIMESTAMP_X']); 298 | 299 | return $stack; 300 | } 301 | 302 | /** 303 | * @depends testCanSkipUnmodifiedObjectSave 304 | * @param array $stack 305 | * @return array 306 | * @throws AnnotationException 307 | * @throws ReflectionException 308 | * @throws Exception 309 | */ 310 | public function testDateTimeFormats(array $stack) 311 | { 312 | $id = $stack['id']; 313 | 314 | /** @var Book $book */ 315 | $book = EntityMapper::select(Book::class)->where('id', $id)->fetch(); 316 | $this->assertInstanceOf(Book::class, $book); 317 | 318 | $book->publishedAt = '1883-09-12 15:30:59'; 319 | $updatedId = EntityMapper::save($book); 320 | $this->assertEquals($id, $updatedId); 321 | 322 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 323 | $this->assertInstanceOf(_CIBElement::class, $element); 324 | $properties = $element->GetProperties(); 325 | 326 | $this->assertEquals( 327 | DateTime::createFromFormat('d.m.Y H:i:s', '12.09.1883 15:30:59')->getTimestamp(), 328 | (new DateTime($properties['published_at']['VALUE']))->getTimestamp() 329 | ); 330 | 331 | $book->publishedAt = DateTime::createFromFormat('d.m.Y H:i:s', '10.08.1883 00:00:00')->getTimestamp(); 332 | $updatedId = EntityMapper::save($book); 333 | $this->assertEquals($id, $updatedId); 334 | 335 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 336 | $this->assertInstanceOf(_CIBElement::class, $element); 337 | $properties = $element->GetProperties(); 338 | 339 | $this->assertEquals( 340 | DateTime::createFromFormat('d.m.Y H:i:s', '10.08.1883 00:00:00')->getTimestamp(), 341 | (new DateTime($properties['published_at']['VALUE']))->getTimestamp() 342 | ); 343 | 344 | $dateTime = DateTime::createFromFormat('d.m.Y H:i:s', '10.08.1883 00:00:00'); 345 | $book->publishedAt = BitrixDateTime::createFromPhp($dateTime); 346 | $updatedId = EntityMapper::save($book); 347 | $this->assertEquals($id, $updatedId); 348 | 349 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 350 | $this->assertInstanceOf(_CIBElement::class, $element); 351 | $properties = $element->GetProperties(); 352 | 353 | $this->assertEquals( 354 | DateTime::createFromFormat('d.m.Y H:i:s', '10.08.1883 00:00:00')->getTimestamp(), 355 | (new DateTime($properties['published_at']['VALUE']))->getTimestamp() 356 | ); 357 | 358 | $book->publishedAt = null; 359 | $updatedId = EntityMapper::save($book); 360 | $this->assertEquals($id, $updatedId); 361 | 362 | $element = CIBlockElement::GetList(null, ['ID' => $id])->GetNextElement(); 363 | $this->assertInstanceOf(_CIBElement::class, $element); 364 | $properties = $element->GetProperties(); 365 | 366 | $this->assertEmpty($properties['published_at']['VALUE']); 367 | 368 | return $stack; 369 | } 370 | } -------------------------------------------------------------------------------- /tests/src/FunctionalTest/SchemaBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(SchemaBuilder::build($entityMap)); 47 | } 48 | 49 | /** 50 | * @depends testCanBuildSchema 51 | */ 52 | public function testIsSchemaCorrect() 53 | { 54 | $infoBlock = CIBlock::GetList(null, [ 55 | '=TYPE' => 'test_entity', 56 | '=CODE' => 'books', 57 | 'CHECK_PERMISSIONS' => 'N' 58 | ])->Fetch(); 59 | 60 | $this->assertTrue(is_array($infoBlock)); 61 | $this->assertArrayHasKey('ID', $infoBlock); 62 | $this->assertNotEmpty($infoBlock['ID']); 63 | 64 | $rs = CIBlockProperty::GetList(null, [ 65 | 'IBLOCK_ID' => $infoBlock['ID'] 66 | ]); 67 | 68 | $this->assertInstanceOf(CIBlockPropertyResult::class, $rs); 69 | 70 | $properties = []; 71 | while ($prop = $rs->Fetch()) { 72 | $properties[$prop['CODE']] = $prop; 73 | } 74 | 75 | $this->assertArrayHasKey('co_authors', $properties); 76 | $this->assertEquals('Соавторы', $properties['co_authors']['NAME']); 77 | $this->assertEquals('E', $properties['co_authors']['PROPERTY_TYPE']); 78 | $this->assertEmpty($properties['co_authors']['USER_TYPE']); 79 | $this->assertEquals('Y', $properties['co_authors']['MULTIPLE']); 80 | 81 | $this->assertArrayHasKey('published_at', $properties); 82 | $this->assertEquals('Опубликована', $properties['published_at']['NAME']); 83 | $this->assertEquals('S', $properties['published_at']['PROPERTY_TYPE']); 84 | $this->assertEquals('DateTime', $properties['published_at']['USER_TYPE']); 85 | $this->assertEquals('N', $properties['published_at']['MULTIPLE']); 86 | 87 | $this->assertArrayHasKey('is_bestseller', $properties); 88 | $this->assertEquals('Бестселлер', $properties['is_bestseller']['NAME']); 89 | $this->assertEquals('L', $properties['is_bestseller']['PROPERTY_TYPE']); 90 | $this->assertEmpty($properties['is_bestseller']['USER_TYPE']); 91 | $this->assertEquals('N', $properties['is_bestseller']['MULTIPLE']); 92 | $this->assertEquals('C', $properties['is_bestseller']['LIST_TYPE']); 93 | 94 | $enumRs = CIBlockProperty::GetPropertyEnum($properties['is_bestseller']['ID']); 95 | $this->assertInstanceOf(CDBResult::class, $enumRs); 96 | 97 | $propEnum = []; 98 | while ($entry = $enumRs->Fetch()) { 99 | $propEnum[] = $entry; 100 | } 101 | 102 | $this->assertCount(1, $propEnum); 103 | $enumYesOption = reset($propEnum); 104 | $this->assertTrue(is_array($enumYesOption)); 105 | $this->assertArrayHasKey('XML_ID', $enumYesOption); 106 | $this->assertEquals('Y', $enumYesOption['XML_ID']); 107 | $this->assertArrayHasKey('VALUE', $enumYesOption); 108 | $this->assertEquals('Y', $enumYesOption['VALUE']); 109 | 110 | $this->assertArrayHasKey('pages_num', $properties); 111 | $this->assertEquals('Кол-во страниц', $properties['pages_num']['NAME']); 112 | $this->assertEquals('N', $properties['pages_num']['PROPERTY_TYPE']); 113 | $this->assertEmpty($properties['pages_num']['USER_TYPE']); 114 | $this->assertEquals('N', $properties['pages_num']['MULTIPLE']); 115 | 116 | $this->assertArrayHasKey('tags', $properties); 117 | $this->assertEquals('Теги', $properties['tags']['NAME']); 118 | $this->assertEquals('S', $properties['tags']['PROPERTY_TYPE']); 119 | $this->assertEmpty($properties['tags']['USER_TYPE']); 120 | $this->assertEquals('Y', $properties['tags']['MULTIPLE']); 121 | 122 | $this->assertArrayHasKey('republications_at', $properties); 123 | $this->assertEquals('Переиздания', $properties['republications_at']['NAME']); 124 | $this->assertEquals('S', $properties['republications_at']['PROPERTY_TYPE']); 125 | $this->assertEquals('DateTime', $properties['republications_at']['USER_TYPE']); 126 | $this->assertEquals('Y', $properties['republications_at']['MULTIPLE']); 127 | 128 | $this->assertArrayHasKey('cover', $properties); 129 | $this->assertEquals('Обложка', $properties['cover']['NAME']); 130 | $this->assertEquals('F', $properties['cover']['PROPERTY_TYPE']); 131 | $this->assertEmpty($properties['cover']['USER_TYPE']); 132 | $this->assertEquals('N', $properties['cover']['MULTIPLE']); 133 | } 134 | 135 | /** 136 | * @depends testIsSchemaCorrect 137 | * @throws AnnotationException 138 | * @throws ReflectionException 139 | */ 140 | public function testCanRebuildSchema() 141 | { 142 | $infoBlock = CIBlock::GetList(null, [ 143 | '=TYPE' => 'test_entity', 144 | '=CODE' => 'books', 145 | 'CHECK_PERMISSIONS' => 'N' 146 | ])->Fetch(); 147 | 148 | $this->assertTrue(is_array($infoBlock)); 149 | $this->assertArrayHasKey('ID', $infoBlock); 150 | $this->assertNotEmpty($infoBlock['ID']); 151 | 152 | $isBestsellerProp = CIBlockProperty::GetList(null, [ 153 | 'IBLOCK_ID' => $infoBlock['ID'], 154 | 'CODE' => 'is_bestseller' 155 | ])->Fetch(); 156 | 157 | $this->assertNotEmpty($isBestsellerProp['ID']); 158 | $isDeleted = CIBlockPropertyEnum::DeleteByPropertyID($isBestsellerProp['ID']); 159 | $this->assertNotEmpty($isDeleted); 160 | 161 | $this->testCanBuildSchema(); 162 | $this->testIsSchemaCorrect(); 163 | } 164 | } -------------------------------------------------------------------------------- /tests/src/FunctionalTest/SelectTest.php: -------------------------------------------------------------------------------- 1 | 'test_entity', 55 | '=CODE' => 'authors', 56 | 'CHECK_PERMISSIONS' => 'N' 57 | ])->Fetch(); 58 | 59 | self::assertNotEmpty($iBlock['ID']); 60 | 61 | $coAuthors = [ 62 | ['NAME' => 'Р. Л. Стивенсон'], 63 | ['NAME' => 'Т. Пратчетт'], 64 | ['NAME' => 'Неизвестный автор'], 65 | ['NAME' => 'Неизвестный автор 2'] 66 | ]; 67 | 68 | $ids = []; 69 | foreach ($coAuthors as $fields) { 70 | $fields['IBLOCK_ID'] = $iBlock['ID']; 71 | $cIBlockElement = new CIBlockElement(); 72 | $id = $cIBlockElement->Add($fields); 73 | self::assertNotEmpty($id, strip_tags($cIBlockElement->LAST_ERROR)); 74 | $ids[$fields['NAME']] = $id; 75 | } 76 | 77 | return $ids; 78 | } 79 | 80 | private static function addBooks() 81 | { 82 | $iBlock = CIBlock::GetList(null, [ 83 | '=TYPE' => 'test_entity', 84 | '=CODE' => 'books', 85 | 'CHECK_PERMISSIONS' => 'N' 86 | ])->Fetch(); 87 | 88 | self::assertNotEmpty($iBlock['ID']); 89 | 90 | $ids = []; 91 | foreach (self::getBookFields() as $fields) { 92 | $fields['IBLOCK_ID'] = $iBlock['ID']; 93 | $cIBlockElement = new CIBlockElement(); 94 | $id = $cIBlockElement->Add($fields); 95 | self::assertNotEmpty($id, strip_tags($cIBlockElement->LAST_ERROR)); 96 | $ids[] = $id; 97 | } 98 | 99 | return $ids; 100 | } 101 | 102 | private static function getBookFields() 103 | { 104 | $yesPropEnum = CIBlockProperty::GetPropertyEnum( 105 | 'is_bestseller', 106 | null, 107 | ['XML_ID' => 'Y', 'VALUE' => 'Y'] 108 | )->Fetch(); 109 | 110 | self::assertNotEmpty($yesPropEnum['ID']); 111 | 112 | return [ 113 | [ 114 | 'NAME' => 'Остров сокровищ', 115 | 'ACTIVE' => 'Y', 116 | 'PROPERTY_VALUES' => [ 117 | 'author' => self::$authorIds['Р. Л. Стивенсон'], 118 | 'co_authors' => [self::$authorIds['Неизвестный автор'], self::$authorIds['Неизвестный автор 2']], 119 | 'is_bestseller' => $yesPropEnum['ID'], 120 | 'pages_num' => 350, 121 | 'tags' => ['приключения', 'пираты'], 122 | 'published_at' => BitrixDateTime::createFromPhp( 123 | DateTime::createFromFormat('d.m.Y H:i:s', '14.06.1883 00:00:00') 124 | ) 125 | ] 126 | ], 127 | [ 128 | 'NAME' => 'Цвет волшебства', 129 | 'ACTIVE' => 'N', 130 | 'PROPERTY_VALUES' => [ 131 | 'author' => self::$authorIds['Т. Пратчетт'], 132 | 'co_authors' => false, 133 | 'is_bestseller' => false, 134 | 'pages_num' => 300, 135 | 'tags' => ['приключения', 'фентези'], 136 | 'published_at' => BitrixDateTime::createFromPhp( 137 | DateTime::createFromFormat('d.m.Y H:i:s', '01.09.1983 00:00:00') 138 | ) 139 | ] 140 | ] 141 | ]; 142 | } 143 | 144 | /** 145 | * @throws AnnotationException 146 | * @throws ReflectionException 147 | */ 148 | public function testCanIterateResult() 149 | { 150 | $select = Select::from(Book::class); 151 | $this->assertInstanceOf(Select::class, $select); 152 | $this->assertInstanceOf(Generator::class, $select->iterator()); 153 | 154 | /** @var Book[] $books */ 155 | $books = []; 156 | $iteration = 0; 157 | foreach ($select->iterator() as $book) { 158 | $iteration++; 159 | $this->assertLessThanOrEqual(2, $iteration, 'Итератор уходит в бесконечный цикл.'); 160 | $books[] = $book; 161 | } 162 | 163 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 164 | $this->assertCount(2, $books); 165 | $this->assertNotEquals($books[0]->getId(), $books[1]->getId()); 166 | } 167 | 168 | /** 169 | * @throws AnnotationException 170 | * @throws ReflectionException 171 | */ 172 | public function testCanFetchAll() 173 | { 174 | $select = Select::from(Book::class); 175 | $this->assertInstanceOf(Select::class, $select); 176 | $books = $select->fetchAll(); 177 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 178 | $this->assertCount(2, $books); 179 | $this->assertNotEquals($books[0]->getId(), $books[1]->getId()); 180 | } 181 | 182 | /** 183 | * @throws AnnotationException 184 | * @throws ReflectionException 185 | */ 186 | public function testCanFetch() 187 | { 188 | $select = Select::from(Book::class); 189 | $this->assertInstanceOf(Select::class, $select); 190 | 191 | /** @var Book[] $books */ 192 | $books = []; 193 | $iteration = 0; 194 | while ($book = $select->fetch()) { 195 | $iteration++; 196 | $this->assertLessThanOrEqual(2, $iteration, 'Итератор уходит в бесконечный цикл.'); 197 | $books[] = $book; 198 | } 199 | 200 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 201 | $this->assertCount(2, $books); 202 | $this->assertNotEquals($books[0]->getId(), $books[1]->getId()); 203 | } 204 | 205 | /** 206 | * @throws AnnotationException 207 | * @throws ReflectionException 208 | */ 209 | public function testIteratorInterfaceImplementation() 210 | { 211 | $select = Select::from(Book::class); 212 | $this->assertInstanceOf(Select::class, $select); 213 | 214 | /** @var Book[] $books */ 215 | $books = []; 216 | $iteration = 0; 217 | foreach ($select as $book) { 218 | $iteration++; 219 | $this->assertLessThanOrEqual(2, $iteration, 'Итератор уходит в бесконечный цикл.'); 220 | $books[] = $book; 221 | } 222 | 223 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 224 | $this->assertCount(2, $books); 225 | $this->assertNotEquals($books[0]->getId(), $books[1]->getId()); 226 | 227 | $this->assertFalse($select->valid()); 228 | $this->assertNull($select->key()); 229 | $this->assertNull($select->current()); 230 | 231 | $select->rewind(); 232 | $this->assertTrue($select->valid()); 233 | $this->assertEquals(0, $select->key()); 234 | $this->assertInstanceOf(Book::class, $select->current()); 235 | 236 | $select->next(); 237 | $this->assertTrue($select->valid()); 238 | $this->assertEquals(1, $select->key()); 239 | $this->assertInstanceOf(Book::class, $select->current()); 240 | 241 | $select->next(); 242 | $this->assertFalse($select->valid()); 243 | $this->assertNull($select->key()); 244 | $this->assertNull($select->current()); 245 | } 246 | 247 | /** 248 | * @throws AnnotationException 249 | * @throws ReflectionException 250 | */ 251 | public function testCanSelectByPrimaryKey() 252 | { 253 | foreach (self::$bookIds as $id) { 254 | $select = Select::from(Book::class)->where('id', $id); 255 | $this->assertInstanceOf(Select::class, $select); 256 | /** @var Book $book */ 257 | $book = $select->fetch(); 258 | $this->assertInstanceOf(Book::class, $book); 259 | $this->assertEquals($id, $book->getId()); 260 | } 261 | } 262 | 263 | /** 264 | * @throws AnnotationException 265 | * @throws ReflectionException 266 | */ 267 | public function testCanSelectBySubstring() 268 | { 269 | /** @var Book $book */ 270 | $book = Select::from(Book::class)->where('title', '%', 'сокров')->fetch(); 271 | $this->assertInstanceOf(Book::class, $book); 272 | $this->assertEquals('Остров сокровищ', $book->title); 273 | 274 | /** @var Book[] $books */ 275 | $books = Select::from(Book::class)->where('title', '%', 'ст')->fetchAll(); 276 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 277 | $this->assertCount(2, $books); 278 | 279 | /** @var Book[] $books */ 280 | $books = Select::from(Book::class)->where('title', '%', 'undefined')->fetchAll(); 281 | $this->assertCount(0, $books); 282 | 283 | /** @var Book[] $books */ 284 | $books = Select::from(Book::class)->where('tags', 'фентези')->fetchAll(); 285 | $this->assertCount(1, $books); 286 | } 287 | 288 | /** 289 | * @throws AnnotationException 290 | * @throws ReflectionException 291 | */ 292 | public function testCanSelectByBoolean() 293 | { 294 | /** @var Book $book */ 295 | $book = Select::from(Book::class)->where('isShow', true)->fetch(); 296 | $this->assertInstanceOf(Book::class, $book); 297 | $this->assertEquals(true, $book->isShow); 298 | 299 | /** @var Book $book */ 300 | $book = Select::from(Book::class)->where('isShow', false)->fetch(); 301 | $this->assertInstanceOf(Book::class, $book); 302 | $this->assertEquals(false, $book->isShow); 303 | 304 | /** @var Book $book */ 305 | $book = Select::from(Book::class)->where('isBestseller', true)->fetch(); 306 | $this->assertInstanceOf(Book::class, $book); 307 | $this->assertEquals(true, $book->isBestseller); 308 | 309 | /** @var Book $book */ 310 | $book = Select::from(Book::class)->where('isBestseller', false)->fetch(); 311 | $this->assertInstanceOf(Book::class, $book); 312 | $this->assertEquals(false, $book->isBestseller); 313 | } 314 | 315 | /** 316 | * @throws AnnotationException 317 | * @throws ReflectionException 318 | */ 319 | public function testCanSelectByDateTime() 320 | { 321 | /** @var Book[] $books */ 322 | $books = Select::from(Book::class)->where('publishedAt', '14.06.1883')->fetchAll(); 323 | $this->assertCount(1, $books); 324 | $book = reset($books); 325 | $this->assertInstanceOf(Book::class, $book); 326 | $this->assertEquals('14.06.1883', $book->publishedAt->format('d.m.Y')); 327 | 328 | /** @var Book[] $books */ 329 | $books = Select::from(Book::class)->where('publishedAt', '%', '14.06.1883')->fetchAll(); 330 | $this->assertCount(1, $books); 331 | $book = reset($books); 332 | $this->assertInstanceOf(Book::class, $book); 333 | $this->assertEquals('14.06.1883', $book->publishedAt->format('d.m.Y')); 334 | 335 | /** @var Book[] $books */ 336 | $books = Select::from(Book::class)->where('publishedAt', '<', '01.01.1900')->fetchAll(); 337 | $this->assertCount(1, $books); 338 | $book = reset($books); 339 | $this->assertInstanceOf(Book::class, $book); 340 | $this->assertEquals('14.06.1883', $book->publishedAt->format('d.m.Y')); 341 | 342 | /** @var Book[] $books */ 343 | $books = Select::from(Book::class)->where('publishedAt', null)->fetchAll(); 344 | $this->assertCount(0, $books); 345 | 346 | /** @var Book[] $books */ 347 | $books = Select::from(Book::class)->where('publishedAt', new DateTime('01.09.1983 00:00:00'))->fetchAll(); 348 | $this->assertCount(1, $books); 349 | $book = reset($books); 350 | $this->assertInstanceOf(Book::class, $book); 351 | $this->assertEquals('01.09.1983', $book->publishedAt->format('d.m.Y')); 352 | 353 | /** @var Book[] $books */ 354 | $books = Select::from(Book::class)->where('publishedAt', BitrixDateTime::createFromPhp(new DateTime('01.09.1983')))->fetchAll(); 355 | $this->assertCount(1, $books); 356 | $book = reset($books); 357 | $this->assertInstanceOf(Book::class, $book); 358 | $this->assertEquals('01.09.1983', $book->publishedAt->format('d.m.Y')); 359 | } 360 | 361 | /** 362 | * @throws AnnotationException 363 | * @throws ReflectionException 364 | */ 365 | public function testCanSelectByRawFilter() 366 | { 367 | /** @var Book $book */ 368 | $book = Select::from(Book::class)->whereRaw('NAME', 'Остров сокровищ')->fetch(); 369 | $this->assertInstanceOf(Book::class, $book); 370 | $this->assertEquals('Остров сокровищ', $book->title); 371 | 372 | /** @var Book $book */ 373 | $book = Select::from(Book::class)->whereRaw('NAME', '%', 'сокровищ')->fetch(); 374 | $this->assertInstanceOf(Book::class, $book); 375 | $this->assertEquals('Остров сокровищ', $book->title); 376 | } 377 | 378 | /** 379 | * @throws AnnotationException 380 | * @throws ReflectionException 381 | */ 382 | public function testCanSortByProperty() 383 | { 384 | /** @var Book[] $books */ 385 | $books = Select::from(Book::class)->orderBy('pagesNum', 'asc')->fetchAll(); 386 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 387 | 388 | $prev = null; 389 | foreach ($books as $book) { 390 | if ($prev !== null) { 391 | $this->assertTrue($prev <= $book->pagesNum); 392 | } 393 | $prev = $book->pagesNum; 394 | } 395 | 396 | /** @var Book[] $books */ 397 | $books = Select::from(Book::class)->orderBy('pagesNum', 'desc')->fetchAll(); 398 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 399 | 400 | $prev = null; 401 | foreach ($books as $book) { 402 | if ($prev !== null) { 403 | $this->assertTrue($prev >= $book->pagesNum); 404 | } 405 | $prev = $book->pagesNum; 406 | } 407 | } 408 | 409 | /** 410 | * @throws AnnotationException 411 | * @throws ReflectionException 412 | */ 413 | public function testCanSortByField() 414 | { 415 | /** @var Book[] $books */ 416 | $books = Select::from(Book::class)->orderBy('id', 'asc')->fetchAll(); 417 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 418 | 419 | $prev = null; 420 | foreach ($books as $book) { 421 | if ($prev !== null) { 422 | $this->assertTrue($prev <= $book->getId()); 423 | } 424 | $prev = $book->getId(); 425 | } 426 | 427 | /** @var Book[] $books */ 428 | $books = Select::from(Book::class)->orderBy('id', 'desc')->fetchAll(); 429 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 430 | 431 | $prev = null; 432 | foreach ($books as $book) { 433 | if ($prev !== null) { 434 | $this->assertTrue($prev >= $book->getId()); 435 | } 436 | $prev = $book->getId(); 437 | } 438 | } 439 | 440 | /** 441 | * @throws AnnotationException 442 | * @throws ReflectionException 443 | */ 444 | public function testCanSortByBooleanField() 445 | { 446 | /** @var Book[] $books */ 447 | $books = Select::from(Book::class)->orderBy('isShow', 'asc')->fetchAll(); 448 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 449 | 450 | $prev = null; 451 | foreach ($books as $book) { 452 | if ($prev !== null) { 453 | $this->assertTrue($prev <= $book->isShow); 454 | } 455 | $prev = $book->isShow; 456 | } 457 | 458 | /** @var Book[] $books */ 459 | $books = Select::from(Book::class)->orderBy('isShow', 'desc')->fetchAll(); 460 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 461 | 462 | $prev = null; 463 | foreach ($books as $book) { 464 | if ($prev !== null) { 465 | $this->assertTrue($prev >= $book->isShow); 466 | } 467 | $prev = $book->isShow; 468 | } 469 | } 470 | 471 | /** 472 | * @throws AnnotationException 473 | * @throws ReflectionException 474 | */ 475 | public function testCanSortByBooleanProperty() 476 | { 477 | /** @var Book[] $books */ 478 | $books = Select::from(Book::class)->orderBy('isBestseller', 'asc')->fetchAll(); 479 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 480 | 481 | $prev = null; 482 | foreach ($books as $book) { 483 | if ($prev !== null) { 484 | $this->assertTrue($prev <= $book->isBestseller); 485 | } 486 | $prev = $book->isBestseller; 487 | } 488 | 489 | /** @var Book[] $books */ 490 | $books = Select::from(Book::class)->orderBy('isBestseller', 'desc')->fetchAll(); 491 | $this->assertContainsOnlyInstancesOf(Book::class, $books); 492 | 493 | $prev = null; 494 | foreach ($books as $book) { 495 | if ($prev !== null) { 496 | $this->assertTrue($prev >= $book->isBestseller); 497 | } 498 | $prev = $book->isBestseller; 499 | } 500 | } 501 | 502 | /** 503 | * @throws AnnotationException 504 | * @throws ReflectionException 505 | */ 506 | public function testCanReturnChildEntities() 507 | { 508 | /** @var Book $book */ 509 | $book = Select::from(Book::class)->where('title', 'Остров сокровищ')->fetch(); 510 | $this->assertInstanceOf(Book::class, $book); 511 | $this->assertInstanceOf(Author::class, $book->author); 512 | $this->assertEquals('Р. Л. Стивенсон', $book->author->getName()); 513 | $this->assertContainsOnlyInstancesOf(Author::class, $book->coAuthors); 514 | 515 | $coAuthorNames = array_map(function (Author $author) { 516 | return $author->getName(); 517 | }, $book->coAuthors); 518 | 519 | $this->assertEmpty(array_diff($coAuthorNames, ['Неизвестный автор', 'Неизвестный автор 2'])); 520 | $this->assertEmpty(array_diff(['Неизвестный автор', 'Неизвестный автор 2'], $coAuthorNames)); 521 | 522 | /** @var Book $book */ 523 | $book = Select::from(Book::class)->where('title', 'Цвет волшебства')->fetch(); 524 | $this->assertInstanceOf(Book::class, $book); 525 | $this->assertInstanceOf(Author::class, $book->author); 526 | $this->assertEquals('Т. Пратчетт', $book->author->getName()); 527 | $this->assertContainsOnlyInstancesOf(Author::class, $book->coAuthors); 528 | 529 | $coAuthorNames = array_map(function (Author $author) { 530 | return $author->getName(); 531 | }, $book->coAuthors); 532 | 533 | $this->assertEmpty(array_diff($coAuthorNames, [])); 534 | $this->assertEmpty(array_diff([], $coAuthorNames)); 535 | } 536 | } 537 | -------------------------------------------------------------------------------- /tests/src/TestCase.php: -------------------------------------------------------------------------------- 1 | CleanAll(); 28 | $GLOBALS["stackCacheManager"]->CleanAll(); 29 | $staticHtmlCache = StaticHtmlCache::getInstance(); 30 | $staticHtmlCache->deleteAll(); 31 | } 32 | 33 | public static function deleteInfoBlocks() 34 | { 35 | $rs = CIBlock::GetList(null, ['TYPE' => 'test_entity', 'CHECK_PERMISSIONS' => 'N']); 36 | while ($infoBlock = $rs->Fetch()) { 37 | CIBlock::Delete($infoBlock['ID']); 38 | } 39 | } 40 | 41 | /** 42 | * @throws RuntimeException 43 | */ 44 | public static function deleteInfoBlockType() 45 | { 46 | $type = 'test_entity'; 47 | $exist = CIBlockType::GetByID($type)->Fetch(); 48 | if ($exist) { 49 | $isDeleted = CIBlockType::Delete($type); 50 | if (!$isDeleted) { 51 | throw new RuntimeException("Ошибка удаления типа инфоблока $type."); 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @throws RuntimeException 58 | */ 59 | public static function addInfoBlockType() 60 | { 61 | $type = 'test_entity'; 62 | $name = 'Тестирование EntityMapper'; 63 | $exist = CIBlockType::GetByID($type)->Fetch(); 64 | if (!$exist) { 65 | $cIBlockType = new CIBlockType(); 66 | $isAdded = $cIBlockType->Add([ 67 | 'ID' => $type, 68 | 'SECTIONS' => 'Y', 69 | 'IN_RSS' => 'N', 70 | 'LANG' => [ 71 | 'ru' => [ 72 | 'NAME' => $name 73 | ] 74 | ] 75 | ]); 76 | 77 | if (!$isAdded) { 78 | throw new RuntimeException(strip_tags($cIBlockType->LAST_ERROR)); 79 | } 80 | } 81 | } 82 | 83 | public static function addSites() 84 | { 85 | $cultures = []; 86 | foreach (CultureTable::getList([])->fetchAll() as $culture) { 87 | $cultures[$culture['CODE']] = $culture; 88 | } 89 | 90 | if (empty($cultures['en'])) { 91 | throw new LogicException('Culture [en] error.'); 92 | } 93 | 94 | if (empty($cultures['ru'])) { 95 | throw new LogicException('Culture [ru] error.'); 96 | } 97 | 98 | $sites = []; 99 | $rs = CSite::GetList($by, $order); 100 | while ($site = $rs->Fetch()) { 101 | $sites[$site['ID']] = $site; 102 | } 103 | 104 | if (empty($sites['p1'])) { 105 | $cSite = new CSite(); 106 | $cSite->Add([ 107 | 'LID' => 'p1', 108 | 'ACTIVE' => 'Y', 109 | 'NAME' => 'Тестовая компания', 110 | 'DIR' => '/', 111 | 'CHARSET' => 'UTF-8', 112 | 'LANGUAGE_ID' => 'ru', 113 | 'CULTURE_ID' => $cultures['ru']['ID'], 114 | ]); 115 | } 116 | 117 | if (empty($sites['p2'])) { 118 | $cSite = new CSite(); 119 | $cSite->Add([ 120 | 'LID' => 'p2', 121 | 'ACTIVE' => 'Y', 122 | 'NAME' => 'Test company', 123 | 'DIR' => '/', 124 | 'CHARSET' => 'UTF-8', 125 | 'LANGUAGE_ID' => 'en', 126 | 'CULTURE_ID' => $cultures['en']['ID'], 127 | ]); 128 | } 129 | } 130 | 131 | public static function deleteSites() 132 | { 133 | CSite::Delete('p1'); 134 | CSite::Delete('p2'); 135 | } 136 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/Annotation/Property/FieldTest.php: -------------------------------------------------------------------------------- 1 | Field::TYPE_INTEGER, 19 | 'XML_ID' => Field::TYPE_STRING, 20 | 'NAME' => Field::TYPE_STRING, 21 | 'SORT' => Field::TYPE_INTEGER, 22 | 'ACTIVE' => Field::TYPE_BOOLEAN, 23 | 'DATE_ACTIVE_FROM' => Field::TYPE_DATETIME, 24 | 'DATE_ACTIVE_TO' => Field::TYPE_DATETIME, 25 | 'PREVIEW_PICTURE' => Field::TYPE_FILE, 26 | 'DETAIL_PICTURE' => Field::TYPE_FILE, 27 | 'PREVIEW_TEXT' => Field::TYPE_STRING, 28 | 'DETAIL_TEXT' => Field::TYPE_STRING 29 | ]; 30 | 31 | $fieldRef = new ReflectionClass(Field::class); 32 | $methodRef = $fieldRef->getMethod('getTypeByCode'); 33 | $methodRef->setAccessible(true); 34 | 35 | foreach ($map as $code => $expectedType) { 36 | $type = $methodRef->invoke(null, $code); 37 | $this->assertEquals($expectedType, $type); 38 | } 39 | 40 | $methodRef->setAccessible(false); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/EntityMapperTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 20 | EntityMapper::save(null); 21 | } 22 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/Map/EntityMapTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 23 | EntityMap::fromClass(WithoutInfoBlockAnnotation::class); 24 | } 25 | 26 | /** 27 | * @throws AnnotationException 28 | * @throws ReflectionException 29 | */ 30 | public function testCanAssertOnConflictedAnnotations() 31 | { 32 | $this->expectException(InvalidArgumentException::class); 33 | EntityMap::fromClass(WithConflictPropertyAnnotations::class); 34 | } 35 | 36 | /** 37 | * @throws AnnotationException 38 | * @throws ReflectionException 39 | */ 40 | public function testCanAssertOnTryingGetNotMappedProperty() 41 | { 42 | $entityMap = EntityMap::fromClass(Book::class); 43 | $this->expectException(InvalidArgumentException::class); 44 | $entityMap->getProperty('notMappedProperty'); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/Query/DataBuilderTest.php: -------------------------------------------------------------------------------- 1 | getMethod('assert'); 20 | $assertRef->setAccessible(true); 21 | $this->expectException(InvalidArgumentException::class); 22 | $assertRef->invoke(null, false, 'Test assertion.'); 23 | } 24 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/Query/RawResultTest.php: -------------------------------------------------------------------------------- 1 | getMethod('assert'); 26 | $assertRef->setAccessible(true); 27 | $this->expectException(InvalidArgumentException::class); 28 | $assertRef->invoke(null, false, 'Test assertion.'); 29 | } 30 | 31 | /** 32 | * @throws ReflectionException 33 | */ 34 | public function testNoAssertOnSuccess() 35 | { 36 | $this->expectNotToPerformAssertions(); 37 | $rawResultRef = new ReflectionClass(RawResult::class); 38 | $assertRef = $rawResultRef->getMethod('assert'); 39 | $assertRef->setAccessible(true); 40 | $assertRef->invoke(null, true, 'Test assertion.'); 41 | } 42 | 43 | public function testCanAssertOnGetField() 44 | { 45 | $rawResult = new RawResult(1, 2, ['title' => 'Остров сокровищ', 'author' => 'Р. Л. Стивенсон']); 46 | $this->expectException(InvalidArgumentException::class); 47 | $rawResult->getField('undefined'); 48 | } 49 | 50 | public function testCanGetField() 51 | { 52 | $rawResult = new RawResult(1, 2, ['title' => 'Остров сокровищ', 'author' => 'Р. Л. Стивенсон']); 53 | $this->assertEquals(1, $rawResult->getId()); 54 | $this->assertEquals(2, $rawResult->getInfoBlockId()); 55 | $this->assertEquals('Остров сокровищ', $rawResult->getField('title')); 56 | $this->assertEquals('Р. Л. Стивенсон', $rawResult->getField('author')); 57 | $this->assertTrue(is_array($rawResult->getData())); 58 | } 59 | 60 | /** 61 | * @throws ReflectionException 62 | */ 63 | public function testCanNormalizeValues() 64 | { 65 | $map = [ 66 | [null, PropertyAnnotationInterface::TYPE_STRING, null], 67 | ['string', PropertyAnnotationInterface::TYPE_STRING, 'string'], 68 | ['', PropertyAnnotationInterface::TYPE_STRING, ''], 69 | ['Y', PropertyAnnotationInterface::TYPE_BOOLEAN, true], 70 | ['1', PropertyAnnotationInterface::TYPE_BOOLEAN, true], 71 | ['notEmptyString', PropertyAnnotationInterface::TYPE_BOOLEAN, true], 72 | ['N', PropertyAnnotationInterface::TYPE_BOOLEAN, false], 73 | ['0', PropertyAnnotationInterface::TYPE_BOOLEAN, false], 74 | ['', PropertyAnnotationInterface::TYPE_BOOLEAN, false], 75 | [3, PropertyAnnotationInterface::TYPE_INTEGER, 3], 76 | ['3', PropertyAnnotationInterface::TYPE_INTEGER, 3], 77 | [3.14, PropertyAnnotationInterface::TYPE_FLOAT, 3.14], 78 | ['3.14', PropertyAnnotationInterface::TYPE_FLOAT, 3.14], 79 | ['notEmptyString', PropertyAnnotationInterface::TYPE_INTEGER, 0], 80 | [ 81 | '2014-11-03 12:30:59', 82 | PropertyAnnotationInterface::TYPE_DATETIME, 83 | DateTime::createFromFormat('Y-m-d H:i:s', '2014-11-03 12:30:59') 84 | ], 85 | [ 86 | '03.11.2014 12:30:59', 87 | PropertyAnnotationInterface::TYPE_DATETIME, 88 | DateTime::createFromFormat('Y-m-d H:i:s', '2014-11-03 12:30:59') 89 | ], 90 | ]; 91 | 92 | $rawResultRef = new ReflectionClass(RawResult::class); 93 | $normalizeRef = $rawResultRef->getMethod('normalizeValue'); 94 | $normalizeRef->setAccessible(true); 95 | 96 | foreach ($map as $entry) { 97 | list($rawValue, $type, $expected) = $entry; 98 | 99 | $object = new stdClass(); 100 | $propAnnotation = new Property(['type' => $type, 'code' => $type]); 101 | $object->{$propAnnotation->getCode()} = $rawValue; 102 | $propRef = new ReflectionProperty($object, $propAnnotation->getCode()); 103 | $propertyMap = new PropertyMap($propAnnotation->getCode(), $propAnnotation, $propRef); 104 | 105 | $value = $normalizeRef->invoke(null, $propertyMap, $rawValue); 106 | 107 | if ($type === PropertyAnnotationInterface::TYPE_DATETIME) { 108 | /** @var DateTime $expected */ 109 | $this->assertInstanceOf(DateTime::class, $expected); 110 | /** @var DateTime $value */ 111 | $this->assertInstanceOf(DateTime::class, $value); 112 | $this->assertEquals($expected->getTimestamp(), $value->getTimestamp()); 113 | } else { 114 | $this->assertTrue( 115 | $value === $expected, 116 | "Raw value: $rawValue. Type: $type. Expected: $expected. Value: $value." 117 | ); 118 | } 119 | } 120 | 121 | $normalizeRef->setAccessible(false); 122 | } 123 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/Query/SelectTest.php: -------------------------------------------------------------------------------- 1 | getMethod('assert'); 22 | $assertRef->setAccessible(true); 23 | $this->expectException(InvalidArgumentException::class); 24 | $assertRef->invoke(null, false, 'Test assertion.'); 25 | } 26 | 27 | /** 28 | * @throws AnnotationException 29 | * @throws ReflectionException 30 | */ 31 | public function testCanAssertOnSchemaMissing() 32 | { 33 | self::deleteInfoBlocks(); 34 | $this->expectException(InvalidArgumentException::class); 35 | Select::from(Book::class)->fetch(); 36 | } 37 | } -------------------------------------------------------------------------------- /tests/src/UnitTest/SchemaBuilderTest.php: -------------------------------------------------------------------------------- 1 | getProperty('type'); 26 | $annotationTypePropReflection->setAccessible(true); 27 | $annotationTypePropReflection->setValue($entityMap->getAnnotation(), ''); 28 | $annotationTypePropReflection->setAccessible(false); 29 | 30 | $this->expectException(InvalidArgumentException::class); 31 | SchemaBuilder::build($entityMap); 32 | } 33 | } --------------------------------------------------------------------------------