diff --git a/src/Adapters/D7Adapter.php b/src/Adapters/D7Adapter.php index 3e0b898..62a133d 100644 --- a/src/Adapters/D7Adapter.php +++ b/src/Adapters/D7Adapter.php @@ -2,11 +2,12 @@ namespace Arrilot\BitrixModels\Adapters; +use Bitrix\Main\Entity\ExpressionField; + /** * Class D7Adapter * * @method \Bitrix\Main\DB\Result getList(array $parameters = []) - * @method int getCount(array $filter = []) * @method \Bitrix\Main\Entity\UpdateResult update(int $id, array $fields) * @method \Bitrix\Main\Entity\DeleteResult delete(int $id) * @method \Bitrix\Main\Entity\AddResult add(array $fields) @@ -53,4 +54,15 @@ public function __call($method, $parameters) return $className::$method(...$parameters); } + + public function getCount($filter) + { + $version = explode('.', SM_VERSION); + if ($version[0] <= 15) { + $result = call_user_func_array([$this->className, 'query'], [])->addSelect(new ExpressionField('CNT', 'COUNT(1)'))->setFilter($filter)->exec()->fetch(); + return $result['CNT']; + } else { + return $this->__call('getCount', $filter); + } + } } diff --git a/src/Helpers.php b/src/Helpers.php index a85fce2..08eae26 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -2,6 +2,9 @@ namespace Arrilot\BitrixModels; +use Arrilot\BitrixModels\Models\BaseBitrixModel; +use Illuminate\Support\Collection; + class Helpers { /** @@ -15,4 +18,83 @@ public static function startsWith($haystack, $needle) { return strncmp($haystack, $needle, strlen($needle)) === 0; } + + /** + * @param Collection|BaseBitrixModel[] $primaryModels первичные модели + * @param Collection|BaseBitrixModel[] $relationModels подгруженные связанные модели + * @param string $primaryKey ключ связи в первичной модели + * @param string $relationKey ключ связи в связанной модели + * @param string $relationName название связи в первичной модели + * @param bool $multiple множественная ли это свзязь + */ + public static function assocModels($primaryModels, $relationModels, $primaryKey, $relationKey, $relationName, $multiple) + { + $buckets = static::buildBuckets($relationModels, $relationKey, $multiple); + + foreach ($primaryModels as $i => $primaryModel) { + if ($multiple && is_array($keys = $primaryModel[$primaryKey])) { + $value = []; + foreach ($keys as $key) { + $key = static::normalizeModelKey($key); + if (isset($buckets[$key])) { + $value = array_merge($value, $buckets[$key]); + } + } + } else { + $key = static::normalizeModelKey($primaryModel[$primaryKey]); + $value = isset($buckets[$key]) ? $buckets[$key] : ($multiple ? [] : null); + } + + $primaryModel->populateRelation($relationName, is_array($value) ? (new Collection($value))->keyBy(function ($item) {return $item->id;}) : $value); + } + } + + /** + * Сгруппировать найденные модели + * @param array $models + * @param string $linkKey + * @param bool $multiple + * @return array + */ + protected static function buildBuckets($models, $linkKey, $multiple) + { + $buckets = []; + + foreach ($models as $model) { + $key = $model[$linkKey]; + if (is_scalar($key)) { + $buckets[$key][] = $model; + } elseif (is_array($key)){ + foreach ($key as $k) { + $k = static::normalizeModelKey($k); + $buckets[$k][] = $model; + } + } else { + $key = static::normalizeModelKey($key); + $buckets[$key][] = $model; + } + } + + if (!$multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + + return $buckets; + } + + /** + * @param mixed $value raw key value. + * @return string normalized key value. + */ + protected static function normalizeModelKey($value) + { + if (is_object($value) && method_exists($value, '__toString')) { + // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId` + $value = $value->__toString(); + } + + return $value; + } } diff --git a/src/Models/BaseBitrixModel.php b/src/Models/BaseBitrixModel.php index c8421f6..77945c8 100644 --- a/src/Models/BaseBitrixModel.php +++ b/src/Models/BaseBitrixModel.php @@ -17,6 +17,9 @@ abstract class BaseBitrixModel extends ArrayableModel * @var array */ protected $fieldsSelectedForSave = []; + + /** @var array Поля, хранящиеся в сериализованном виде */ + public static $serializedFields = []; /** * Array of errors that are passed to model events. diff --git a/src/Queries/BaseQuery.php b/src/Queries/BaseQuery.php index 55d6d4c..9e3f046 100644 --- a/src/Queries/BaseQuery.php +++ b/src/Queries/BaseQuery.php @@ -3,6 +3,7 @@ namespace Arrilot\BitrixModels\Queries; use Arrilot\BitrixModels\Models\BaseBitrixModel; +use Arrilot\BitrixModels\ServiceProvider; use BadMethodCallException; use Bitrix\Main\Data\Cache; use Closure; @@ -15,7 +16,9 @@ abstract class BaseQuery { use BaseRelationQuery; - + + public static $cacheDir = '/bitrix-models'; + /** * Query select. * @@ -518,7 +521,7 @@ protected function rememberInCache($key, $minutes, Closure $callback) } $cache = Cache::createInstance(); - if ($cache->initCache($minutes * 60, $key, '/bitrix-models')) { + if ($cache->initCache($minutes * 60, $key, static::$cacheDir)) { $vars = $cache->getVars(); return !empty($vars['isCollection']) ? new Collection($vars['cache']) : $vars['cache']; } @@ -577,4 +580,33 @@ protected function prepareMultiFilter(&$key, &$value) { } + + /** + * Проверка включен ли тегированный кеш + * @return bool + */ + protected function isManagedCacheOn() + { + $config = ServiceProvider::configProvider(); + return $config::GetOptionString('main', 'component_managed_cache_on', 'N') == 'Y'; + } + + public function exec($query) + { + $queryType = 'BaseQuery::exec'; + + $callback = function () use ($query) { + $rows = []; + $result = \Bitrix\Main\Application::getConnection()->query($query); + $modelName = $this->modelName; + $result->setSerializedFields($modelName::$serializedFields ?: []); + while ($row = $result->fetch()) { + $this->addItemToResultsUsingKeyBy($rows, new $this->modelName($row['ID'], $row)); + } + + return new Collection($rows); + }; + + return $this->handleCacheIfNeeded(compact('queryType', 'query'), $callback); + } } diff --git a/src/Queries/BaseRelationQuery.php b/src/Queries/BaseRelationQuery.php index 491eca0..893b91d 100644 --- a/src/Queries/BaseRelationQuery.php +++ b/src/Queries/BaseRelationQuery.php @@ -4,6 +4,7 @@ namespace Arrilot\BitrixModels\Queries; +use Arrilot\BitrixModels\Helpers; use Arrilot\BitrixModels\Models\BaseBitrixModel; use Illuminate\Support\Collection; @@ -107,6 +108,7 @@ protected function filterByModels($models) } } + $values = array_filter($values); if (empty($values)) { $this->stopQuery(); } @@ -134,7 +136,7 @@ protected function filterByModels($models) public function findWith($with, &$models) { // --- получаем модель, на основании которой будем брать запросы релейшенов - $primaryModel = reset($models); + $primaryModel = $models->first(); if (!$primaryModel instanceof BaseBitrixModel) { $primaryModel = $this->model; } @@ -195,83 +197,9 @@ public function populateRelation($name, &$primaryModels) $this->filterByModels($primaryModels); $models = $this->getList(); - $buckets = $this->buildBuckets($models, $this->localKey); - - foreach ($primaryModels as $i => $primaryModel) { - if ($this->multiple && is_array($keys = $primaryModel[$this->foreignKey])) { - $value = []; - foreach ($keys as $key) { - $key = $this->normalizeModelKey($key); - if (isset($buckets[$key])) { - $value = array_merge($value, $buckets[$key]); - } - } - } else { - $key = $this->getModelKey($primaryModel, $this->foreignKey); - $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); - } - - $primaryModel->populateRelation($name, is_array($value) ? (new Collection($value))->keyBy(function ($item) {return $item->id;}) : $value); - } + + Helpers::assocModels($primaryModels, $models, $this->foreignKey, $this->localKey, $name, $this->multiple); return $models; } - - /** - * Сгруппировать найденные модели - * @param array $models - * @param array|string $linkKeys - * @param bool $checkMultiple - * @return array - */ - private function buildBuckets($models, $linkKeys, $checkMultiple = true) - { - $buckets = []; - - foreach ($models as $model) { - $key = $this->getModelKey($model, $linkKeys); - $buckets[$key][] = $model; - } - - if ($checkMultiple && !$this->multiple) { - foreach ($buckets as $i => $bucket) { - $buckets[$i] = reset($bucket); - } - } - - return $buckets; - } - - /** - * Получить значение атрибутов в виде строки - * @param BaseBitrixModel $model - * @param array|string $attributes - * @return string - */ - private function getModelKey($model, $attributes) - { - $key = []; - foreach ((array)$attributes as $attribute) { - $key[] = $this->normalizeModelKey($model[$attribute]); - } - if (count($key) > 1) { - return serialize($key); - } - $key = reset($key); - return is_scalar($key) ? $key : serialize($key); - } - - /** - * @param mixed $value raw key value. - * @return string normalized key value. - */ - private function normalizeModelKey($value) - { - if (is_object($value) && method_exists($value, '__toString')) { - // ensure matching to special objects, which are convertable to string, for cross-DBMS relations, for example: `|MongoId` - $value = $value->__toString(); - } - - return $value; - } } diff --git a/src/Queries/ElementQuery.php b/src/Queries/ElementQuery.php index 9f09ac2..bfe8620 100644 --- a/src/Queries/ElementQuery.php +++ b/src/Queries/ElementQuery.php @@ -3,6 +3,7 @@ namespace Arrilot\BitrixModels\Queries; use Arrilot\BitrixCacher\Cache; +use Arrilot\BitrixModels\ServiceProvider; use CIBlock; use Illuminate\Support\Collection; use Arrilot\BitrixModels\Models\ElementModel; @@ -159,6 +160,12 @@ protected function loadModels() list($select, $chunkQuery) = $this->multiplySelectForMaxJoinsRestrictionIfNeeded($select); $callback = function() use ($sort, $filter, $groupBy, $navigation, $select, $chunkQuery) { + if (static::isManagedCacheOn()) { + ServiceProvider::cacheManagerProvider()->StartTagCache(static::$cacheDir); + ServiceProvider::cacheManagerProvider()->RegisterTag("iblock_id_new"); + } + + if ($chunkQuery) { $itemsChunks = []; foreach ($select as $chunkIndex => $selectForChunk) { @@ -176,6 +183,11 @@ protected function loadModels() $this->addItemToResultsUsingKeyBy($items, new $this->modelName($arItem['ID'], $arItem)); } } + + + if (static::isManagedCacheOn()) { + ServiceProvider::cacheManagerProvider()->EndTagCache(); + } return new Collection($items); }; @@ -232,31 +244,31 @@ public function count() return $this->handleCacheIfNeeded(compact('filter', 'queryType'), $callback); } - -// /** -// * Normalize properties's format converting it to 'PROPERTY_"CODE"_VALUE'. -// * -// * @param array $fields -// * -// * @return null -// */ -// protected function normalizePropertyResultFormat(&$fields) -// { -// if (empty($fields['PROPERTIES'])) { -// return; -// } -// -// foreach ($fields['PROPERTIES'] as $code => $prop) { -// $fields['PROPERTY_'.$code.'_VALUE'] = $prop['VALUE']; -// $fields['~PROPERTY_'.$code.'_VALUE'] = $prop['~VALUE']; -// $fields['PROPERTY_'.$code.'_DESCRIPTION'] = $prop['DESCRIPTION']; -// $fields['~PROPERTY_'.$code.'_DESCRIPTION'] = $prop['~DESCRIPTION']; -// $fields['PROPERTY_'.$code.'_VALUE_ID'] = $prop['PROPERTY_VALUE_ID']; -// if (isset($prop['VALUE_ENUM_ID'])) { -// $fields['PROPERTY_'.$code.'_ENUM_ID'] = $prop['VALUE_ENUM_ID']; -// } -// } -// } + + /** + * Normalize properties's format converting it to 'PROPERTY_"CODE"_VALUE'. + * + * @param array $fields + * + * @return null + */ + protected function normalizePropertyResultFormat(&$fields) + { + if (empty($fields['PROPERTIES'])) { + return; + } + + foreach ($fields['PROPERTIES'] as $code => $prop) { + $fields['PROPERTY_'.$code.'_VALUE'] = $prop['VALUE']; + $fields['~PROPERTY_'.$code.'_VALUE'] = $prop['~VALUE']; + $fields['PROPERTY_'.$code.'_DESCRIPTION'] = $prop['DESCRIPTION']; + $fields['~PROPERTY_'.$code.'_DESCRIPTION'] = $prop['~DESCRIPTION']; + $fields['PROPERTY_'.$code.'_VALUE_ID'] = $prop['PROPERTY_VALUE_ID']; + if (isset($prop['VALUE_ENUM_ID'])) { + $fields['PROPERTY_'.$code.'_ENUM_ID'] = $prop['VALUE_ENUM_ID']; + } + } + } /** * Normalize filter before sending it to getList. @@ -344,4 +356,58 @@ protected function mergeChunks($chunks) return $items; } + + protected function performFetchUsingSelectedMethod($rsItems) + { + if ($this->fetchUsing['method'] === 'GetNextElement') { + /** @var \_CIBElement $elItem */ + $elItem = $rsItems->GetNextElement(); + + if (!$elItem) { + return false; + } + + $arItem = $elItem->GetFields(); + $arItem['PROPERTIES'] = $elItem->GetProperties(); + $this->normalizePropertyResultFormat($arItem); + unset($arItem['PROPERTIES']); + } else { + $arItem = parent::performFetchUsingSelectedMethod($rsItems); + } + + return $arItem; + } + + /** + * Set fetch using from string or array. + * + * @param string|array $methodAndParams + * @return $this + */ + public function fetchUsing($methodAndParams) + { + // simple case + if (is_string($methodAndParams) || empty($methodAndParams['method'])) { + if (in_array($methodAndParams, ['GetNextElement', 'getNextElement'])) { + $this->fetchUsing = ['method' => 'GetNextElement', 'params' => [true, true]]; + $this->select(['FIELDS', 'PROPERTY_*']); + } else { + parent::fetchUsing($methodAndParams); + } + + return $this; + } + + // complex case + if (in_array($methodAndParams['method'], ['GetNextElement', 'getNextElement'])) { + $bTextHtmlAuto = isset($methodAndParams['params'][0]) ? $methodAndParams['params'][0] : true; + $useTilda = isset($methodAndParams['params'][1]) ? $methodAndParams['params'][1] : true; + $this->fetchUsing = ['method' => 'GetNextElement', 'params' => [$bTextHtmlAuto, $useTilda]]; + $this->select(['FIELDS', 'PROPERTY_*']); + } else { + parent::fetchUsing($methodAndParams); + } + + return $this; + } } diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 8e14110..08a852f 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -14,8 +14,9 @@ class ServiceProvider { + protected static $configProvider; + protected static $cacheManagerProvider; public static $illuminateDatabaseIsUsed = false; - /** * Register the service provider. * @@ -202,4 +203,39 @@ private static function addEventListenersForHelpersHighloadblockTables(Capsule $ } }); } + + public static function registerConfigProvider($provider) + { + static::$configProvider = $provider; + } + + /** + * @return \COption + */ + public static function configProvider() + { + if (!static::$configProvider) { + static::$configProvider = new \COption(); + } + + return static::$configProvider; + } + + public static function registerCacheManagerProvider($provider) + { + static::$cacheManagerProvider = $provider; + } + + /** + * @return \CCacheManager + */ + public static function cacheManagerProvider() + { + if (!static::$cacheManagerProvider) { + global $CACHE_MANAGER; + static::$cacheManagerProvider = $CACHE_MANAGER; + } + + return static::$cacheManagerProvider; + } } diff --git a/tests/ElementModelTest.php b/tests/ElementModelTest.php index 65f3f07..1720faa 100644 --- a/tests/ElementModelTest.php +++ b/tests/ElementModelTest.php @@ -14,6 +14,7 @@ public function setUp() { TestElement::$bxObject = m::mock('obj'); ElementQuery::$cIblockObject = m::mock('cIblockObject'); + parent::setUp(); } public function tearDown() diff --git a/tests/SectionModelTest.php b/tests/SectionModelTest.php index 76b87be..407442a 100644 --- a/tests/SectionModelTest.php +++ b/tests/SectionModelTest.php @@ -10,6 +10,7 @@ class SectionModelTest extends TestCase public function setUp() { TestSection::$bxObject = m::mock('obj'); + parent::setUp(); } public function tearDown() diff --git a/tests/Stubs/COption.php b/tests/Stubs/COption.php new file mode 100644 index 0000000..94966db --- /dev/null +++ b/tests/Stubs/COption.php @@ -0,0 +1,23 @@ + [ + 'component_managed_cache_on' => 'Y', + ] + ]; + + public static function GetOptionInt($module_id, $name, $def = "", $site = false) + { + return intval(static::GetOptionString($module_id, $name, $def, $site)); + } + + public static function GetOptionString($module_id, $name, $def = "", $site = false) + { + return static::$config[$module_id][$name] ?: $def; + } +} \ No newline at end of file diff --git a/tests/Stubs/CacheManager.php b/tests/Stubs/CacheManager.php new file mode 100644 index 0000000..e410529 --- /dev/null +++ b/tests/Stubs/CacheManager.php @@ -0,0 +1,20 @@ +