[ 'className' => 'MetaFields', 'foreignKey' => 'parent_id', 'bindingKey' => 'id', 'dependent' => true, 'cascadeCallbacks' => true, 'saveStrategy' => 'append', 'propertyName' => 'meta_fields', ], 'modelAssoc' => [ 'foreignKey' => 'parent_id', 'bindingKey' => 'id', ], 'metaTemplateFieldCounter' => ['counter'], 'implementedEvents' => [ 'Model.beforeMarshal' => 'beforeMarshal', 'Model.beforeFind' => 'beforeFind', 'Model.beforeSave' => 'beforeSave', ], 'implementedMethods' => [ 'normalizeMetafields' => 'normalizeMetafields', 'buildMetaFieldQuerySnippetForMatchingParent' => 'buildQuerySnippetForMatchingParent', ], 'implementedFinders' => [ 'metafield' => 'findMetafield', ], ]; private $aliasScope = null; private $typeHandlers = []; private $_metaTemplateFieldTable; public function initialize(array $config): void { $this->bindAssociations(); $this->_metaTemplateFieldTable = $this->_table->MetaFields->MetaTemplateFields; $this->_metaTemplateTable = $this->_table->MetaFields->MetaTemplates; $this->loadTypeHandlers(); } private function loadTypeHandlers() { if (empty($this->typeHandlers)) { $this->typeHandlers = $this->_metaTemplateFieldTable->getTypeHandlers(); } } public function getTypeHandlers(): array { return $this->typeHandlers; } public function getScope() { if (is_null($this->aliasScope)) { $this->aliasScope = Inflector::underscore(Inflector::singularize($this->_table->getAlias())); } return $this->aliasScope; } public function bindAssociations() { $config = $this->getConfig(); $metaFieldsAssoc = $config['metaFieldsAssoc']; $modelAssoc = $config['modelAssoc']; $table = $this->_table; $tableAlias = $this->_table->getAlias(); $assocConditions = [ 'MetaFields.scope' => $this->getScope() ]; if (!$table->hasAssociation('MetaFields')) { $table->hasMany('MetaFields', array_merge( $metaFieldsAssoc, [ 'conditions' => $assocConditions ] )); } if (!$table->MetaFields->hasAssociation($tableAlias)) { $table->MetaFields->belongsTo($tableAlias, array_merge( $modelAssoc, [ 'className' => get_class($table), ] )); } } public function beforeMarshal($event, $data, $options) { $property = $this->getConfig('metaFieldsAssoc.propertyName'); $options['accessibleFields'][$property] = true; $options['associated']['MetaFields']['accessibleFields']['id'] = true; if (isset($data[$property])) { if (!empty($data[$property])) { $data[$property] = $this->normalizeMetafields($data[$property]); } } } public function beforeSave($event, $entity, $options) { if (empty($entity->metaFields)) { return; } } public function normalizeMetafields($metaFields) { return $metaFields; } /** * Usage: * $this->{$model}->find('metaField', [ * ['meta_template_id' => 1, 'field' => 'email', 'value' => '%@domain.test'], * ['meta_template_id' => 1, 'field' => 'country_code', 'value' => '!LU'], * ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+2'], * ]) * $this->{$model}->find('metaField', [ * 'AND' => [ * ['meta_template_id' => 1, 'field' => 'email', 'value' => '%@domain.test'], * 'OR' => [ * ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+1'], * ['meta_template_id' => 1, 'field' => 'time_zone', 'value' => 'UTC+2'], * ], * ], * ]) */ public function findMetafield(Query $query, array $filters) { $conditions = $this->buildQuerySnippetForMatchingParent($filters); $query->where($conditions); return $query; } public function buildQuerySnippetForMatchingParent(array $filters): array { if (empty($filters)) { return []; } if (count(array_filter(array_keys($filters), 'is_string'))) { $filters = [$filters]; } $conjugatedFilters = $this->buildConjugatedFilters($filters); $conditions = $this->buildConjugatedQuerySnippet($conjugatedFilters); return $conditions; } protected function buildConjugatedFilters(array $filters): array { $conjugatedFilters = []; foreach ($filters as $operator => $subFilters) { if (is_numeric($operator)) { $conjugatedFilters[] = $subFilters; } else { if (!empty($subFilters)) { $conjugatedFilters[$operator] = $this->buildConjugatedFilters($subFilters); } } } return $conjugatedFilters; } protected function buildConjugatedQuerySnippet(array $conjugatedFilters, string $parentOperator='AND'): array { $conditions = []; if (empty($conjugatedFilters['AND']) && empty($conjugatedFilters['OR'])) { if (count(array_filter(array_keys($conjugatedFilters), 'is_string')) > 0) { $conditions = $this->buildComposedQuerySnippet([$conjugatedFilters]); } else { $conditions = $this->buildComposedQuerySnippet($conjugatedFilters, $parentOperator); } } else { foreach ($conjugatedFilters as $subOperator => $subFilter) { $conditions[$subOperator] = $this->buildConjugatedQuerySnippet($subFilter, $subOperator); } } return $conditions; } protected function buildComposedQuerySnippet(array $filters, string $operator='AND'): array { $conditions = []; foreach ($filters as $filterOperator => $filter) { $subQuery = $this->buildQuerySnippet($filter, true); $modelAlias = $this->_table->getAlias(); $conditions[$operator][] = [$modelAlias . '.id IN' => $subQuery]; } return $conditions; } protected function setQueryExpressionForTextField(QueryExpression $exp, string $field, string $value): QueryExpression { if (substr($value, 0, 1) == '!') { $value = substr($value, 1); $exp->notEq($field, $value); } else if (strpos($value, '%') !== false) { $exp->like($field, $value); } else { $exp->eq($field, $value); } return $exp; } protected function buildQuerySnippet(array $filter): Query { $metaTemplateField = !empty($filter['meta_template_field_id']) ? $this->_metaTemplateFieldTable->get($filter['meta_template_field_id']) : null; $whereClosure = function (QueryExpression $exp) use ($filter, $metaTemplateField) { foreach ($filter as $column => $value) { $keyedColumn = 'MetaFields.' . $column; if ($column == 'value') { $this->setQueryExpressionForField($exp, $keyedColumn, $value, $metaTemplateField); } else { $this->setQueryExpressionForTextField($exp, $keyedColumn, $value); } } return $exp; }; $foreignKey = $this->getConfig('modelAssoc.foreignKey'); $query = $this->_table->MetaFields->find() ->select('MetaFields.' . $foreignKey) ->where($whereClosure); return $query; } protected function setQueryExpressionForField(QueryExpression $exp, string $field, string $value, \App\Model\Entity\MetaTemplateField $metaTemplateField=null): QueryExpression { if (!is_null($metaTemplateField) && isset($this->typeHandlers[$metaTemplateField->type])) { $exp = $this->typeHandlers[$metaTemplateField->type]->setQueryExpression($exp, $value, $metaTemplateField); } else { $exp = $this->setQueryExpressionForTextField($exp, $field, $value); } return $exp; } }