diff --git a/classes/Collection.php b/classes/Collection.php index 848bbe398..c111f644b 100644 --- a/classes/Collection.php +++ b/classes/Collection.php @@ -32,6 +32,10 @@ */ class CollectionCore implements Iterator, ArrayAccess, Countable { + const LEFT_JOIN = 1; + const INNER_JOIN = 2; + const LEFT_OUTER_JOIN = 3; + /** * @var string Object class name */ @@ -75,6 +79,10 @@ class CollectionCore implements Iterator, ArrayAccess, Countable protected $fields = array(); protected $alias = array(); protected $alias_iterator = 0; + protected $join_list = array(); + protected $association_definition = array(); + + const LANG_ALIAS = 'l'; /** * @param string $classname @@ -87,25 +95,45 @@ class CollectionCore implements Iterator, ArrayAccess, Countable $this->definition = ObjectModel::getDefinition($this->classname); if (!isset($this->definition['table'])) - throw new PrestashopException('Miss table in definition for class '.$this->classname); + throw new PrestaShopException('Miss table in definition for class '.$this->classname); else if (!isset($this->definition['primary'])) - throw new PrestashopException('Miss primary in definition for class '.$this->classname); + throw new PrestaShopException('Miss primary in definition for class '.$this->classname); - $alias = $this->generateAlias(); $this->query = new DbQuery(); - $this->query->select($alias.'.*'); - $this->query->from($this->definition['table'], $alias); + } - // If multilang, create association to lang table - // @todo create virtual association with lang in ObjectModel::getDefinition() - if (isset($this->definition['multilang']) && $this->definition['multilang']) + /** + * Join current entity to an associated entity + * + * @param $association Association name + * @param string $on + * @param int $type + * @return Collection + */ + public function join($association, $on = '', $type = null) + { + if (!$association) + return; + + if (!isset($this->join_list[$association])) { - $lang_alias = $this->generateAlias('@lang'); - $this->query->select($lang_alias.'.*'); - $this->query->leftJoin($this->definition['table'].'_lang', $lang_alias, $alias.'.'.$this->definition['primary'].' = '.$lang_alias.'.'. $this->definition['primary']); - if ($this->id_lang) - $this->query->where($lang_alias.'.id_lang = '.(int)$this->id_lang); + $definition = $this->getDefinition($association); + $on = '{'.$definition['asso']['complete_field'].'} = {'.$definition['asso']['complete_foreign_field'].'}'; + $type = self::LEFT_JOIN; + $this->join_list[$association] = array( + 'table' => ($definition['is_lang']) ? $definition['table'].'_lang' : $definition['table'], + 'alias' => $this->generateAlias($association), + 'on' => array(), + ); } + + if ($on) + $this->join_list[$association]['on'][] = $this->parseFields($on); + + if ($type) + $this->join_list[$association]['type'] = $type; + + return $this; } /** @@ -114,10 +142,14 @@ class CollectionCore implements Iterator, ArrayAccess, Countable * @param string $field Field name * @param string $operator List of operators : =, !=, <>, <, <=, >, >=, like, notlike, regexp, notregexp * @param mixed $value + * @param string $type where|having * @return Collection */ - public function where($field, $operator, $value) + public function where($field, $operator, $value, $method = 'where') { + if ($method != 'where' && $method != 'having') + throw new PrestaShopException('Bad method argument for where() method (should be "where" or "having")'); + // Create WHERE clause with an array value (IN, NOT IN) if (is_array($value)) { @@ -125,17 +157,17 @@ class CollectionCore implements Iterator, ArrayAccess, Countable { case '=' : case 'in' : - $this->query->where($this->parseField($field).' IN('.implode(', ', $this->formatValue($value, $field)).')'); + $this->query->$method($this->parseField($field).' IN('.implode(', ', $this->formatValue($value, $field)).')'); break; case '!=' : case '<>' : case 'notin' : - $this->query->where($this->parseField($field).' NOT IN('.implode(', ', $this->formatValue($value, $field)).')'); + $this->query->$method($this->parseField($field).' NOT IN('.implode(', ', $this->formatValue($value, $field)).')'); break; default : - throw new PrestashopException('Operator not supported for array value'); + throw new PrestaShopException('Operator not supported for array value'); } } // Create WHERE clause @@ -152,19 +184,19 @@ class CollectionCore implements Iterator, ArrayAccess, Countable case '<=' : case 'like' : case 'regexp' : - $this->query->where($this->parseField($field).' '.$operator.' '.$this->formatValue($value, $field)); + $this->query->$method($this->parseField($field).' '.$operator.' '.$this->formatValue($value, $field)); break; case 'notlike' : - $this->query->where($this->parseField($field).' NOT LIKE '.$this->formatValue($value, $field)); + $this->query->$method($this->parseField($field).' NOT LIKE '.$this->formatValue($value, $field)); break; case 'notregexp' : - $this->query->where($this->parseField($field).' NOT REGEXP '.$this->formatValue($value, $field)); + $this->query->$method($this->parseField($field).' NOT REGEXP '.$this->formatValue($value, $field)); break; default : - throw new PrestashopException('Operator not supported'); + throw new PrestaShopException('Operator not supported'); } } @@ -175,6 +207,7 @@ class CollectionCore implements Iterator, ArrayAccess, Countable * Add WHERE restriction on query using real SQL syntax * * @param string $sql + * @return Collection */ public function sqlWhere($sql) { @@ -182,6 +215,31 @@ class CollectionCore implements Iterator, ArrayAccess, Countable return $this; } + /** + * Add HAVING restriction on query + * + * @param string $field Field name + * @param string $operator List of operators : =, !=, <>, <, <=, >, >=, like, notlike, regexp, notregexp + * @param mixed $value + * @return Collection + */ + public function having($field, $operator, $value) + { + return $this->where($field, $operator, $value, 'having'); + } + + /** + * Add HAVING restriction on query using real SQL syntax + * + * @param string $sql + * @return Collection + */ + public function sqlHaving($sql) + { + $this->query->having($this->parseFields($sql)); + return $this; + } + /** * Add ORDER BY restriction on query * @@ -193,11 +251,23 @@ class CollectionCore implements Iterator, ArrayAccess, Countable { $order = strtolower($order); if ($order != 'asc' && $order != 'desc') - throw new PrestashopException('Order must be asc or desc'); + throw new PrestaShopException('Order must be asc or desc'); $this->query->orderBy($this->parseField($field).' '.$order); return $this; } + /** + * Add ORDER BY restriction on query using real SQL syntax + * + * @param string $sql + * @return Collection + */ + public function sqlOrderBy($sql) + { + $this->query->orderBy($this->parseFields($sql)); + return $this; + } + /** * Add GROUP BY restriction on query * @@ -210,6 +280,18 @@ class CollectionCore implements Iterator, ArrayAccess, Countable return $this; } + /** + * Add GROUP BY restriction on query using real SQL syntax + * + * @param string $sql + * @return Collection + */ + public function sqlGroupBy($sql) + { + $this->query->orderBy($this->parseFields($sql)); + return $this; + } + /** * Launch sql query to create collection of objects * @@ -222,6 +304,39 @@ class CollectionCore implements Iterator, ArrayAccess, Countable return $this; $this->is_hydrated = true; + $alias = $this->generateAlias(); + //$this->query->select($alias.'.*'); + $this->query->from($this->definition['table'], $alias); + + // If multilang, create association to lang table + if (!empty($this->definition['multilang'])) + { + $this->join(self::LANG_ALIAS); + if ($this->id_lang) + $this->where(self::LANG_ALIAS.'.id_lang', '=', $this->id_lang); + } + + // Add join clause + foreach ($this->join_list as $data) + { + $on = '('.implode(') AND (', $data['on']).')'; + switch ($data['type']) + { + case self::LEFT_JOIN : + $this->query->leftJoin($data['table'], $data['alias'], $on); + break; + + case self::INNER_JOIN : + $this->query->innerJoin($data['table'], $data['alias'], $on); + break; + + case self::LEFT_OUTER_JOIN : + $this->query->leftOuterJoin($data['table'], $data['alias'], $on); + break; + } + } + + // Shall we display query for debug ? if ($display_query) echo $this->query.'
'; @@ -334,7 +449,7 @@ class CollectionCore implements Iterator, ArrayAccess, Countable { $this->getAll(); if (!isset($this->results[$offset])) - throw new PrestashopException('Unknown offset '.$offset.' for collection '.$this->classname); + throw new PrestaShopException('Unknown offset '.$offset.' for collection '.$this->classname); return $this->results[$offset]; } @@ -348,7 +463,7 @@ class CollectionCore implements Iterator, ArrayAccess, Countable public function offsetSet($offset, $value) { if (!$value instanceof $this->classname) - throw new PrestashopException('You cannot add an element which is not an instance of '.$this->classname); + throw new PrestaShopException('You cannot add an element which is not an instance of '.$this->classname); $this->getAll(); if (is_null($offset)) @@ -369,6 +484,69 @@ class CollectionCore implements Iterator, ArrayAccess, Countable unset($this->results[$offset]); } + /** + * Get definition of an association + * + * @param string $association + * @return array + */ + protected function getDefinition($association) + { + if (!$association) + return $this->definition; + + if (!isset($this->association_definition[$association])) + { + $definition = $this->definition; + $split = explode('.', $association); + $is_lang = false; + for ($i = 0, $total_association = count($split); $i < $total_association; $i++) + { + $asso = $split[$i]; + + // Check is current association exists in current definition + if (!isset($definition['associations'][$asso])) + throw new PrestaShopException('Association '.$asso.' not found for class '.$this->definition['classname']); + $current_def = $definition['associations'][$asso]; + + // Special case for lang alias + if ($asso == self::LANG_ALIAS) + { + $is_lang = true; + break; + } + + $classname = (isset($current_def['object'])) ? $current_def['object'] : Tools::toCamelCase($asso, true); + $definition = ObjectModel::getDefinition($classname); + } + + // Get definition of associated entity and add information on current association + $current_def['name'] = $asso; + if (!isset($current_def['object'])) + $current_def['object'] = Tools::toCamelCase($asso, true); + if (!isset($current_def['field'])) + $current_def['field'] = 'id_'.$asso; + if (!isset($current_def['foreign_field'])) + $current_def['foreign_field'] = 'id_'.$asso; + if ($total_association > 1) + { + unset($split[$total_association - 1]); + $current_def['complete_field'] = implode('.', $split).'.'.$current_def['field']; + } + else + $current_def['complete_field'] = $current_def['field']; + $current_def['complete_foreign_field'] = $association.'.'.$current_def['field']; + + $definition['is_lang'] = $is_lang; + $definition['asso'] = $current_def; + $this->association_definition[$association] = $definition; + } + else + $definition = $this->association_definition[$association]; + + return $definition; + } + /** * Parse all fields with {field} syntax in a string * @@ -400,6 +578,7 @@ class CollectionCore implements Iterator, ArrayAccess, Countable * * @param mixed $value * @param string $field Field name + * @return mixed */ protected function formatValue($value, $field) { @@ -425,20 +604,36 @@ class CollectionCore implements Iterator, ArrayAccess, Countable if (!isset($this->fields[$field])) { $split = explode('.', $field); - $association = ''; - $definition = ObjectModel::getDefinition($this->classname); - for ($i = 0, $total_association = count($split) - 1; $i < $total_association; $i++) + $total = count($split); + if ($total > 1) { - // @todo association + $fieldname = $split[$total - 1]; + unset($split[$total - 1]); + $association = implode('.', $split); + } + else + { + $fieldname = $field; + $association = ''; } - $fieldname = $split[$i]; - if ($fieldname == $definition['primary']) + $definition = $this->getDefinition($association); + if ($association && !isset($this->join_list[$association])) + $this->join($association); + + if ($fieldname == $definition['primary'] || (!empty($definition['is_lang']) && $fieldname == 'id_lang')) $type = ObjectModel::TYPE_INT; else { + // Test if field exists if (!isset($definition['fields'][$fieldname])) - throw new PrestashopException('Field '.$fieldname.' not found in class '.$this->classname); + throw new PrestaShopException('Field '.$fieldname.' not found in class '.$definition['classname']); + + // Test field validity for language fields + if (empty($definition['is_lang']) && !empty($definition['fields'][$fieldname]['lang'])) + throw new PrestaShopException('Field '.$fieldname.' is declared as lang field but is used in non multilang context'); + else if (!empty($definition['is_lang']) && empty($definition['fields'][$fieldname]['lang'])) + throw new PrestaShopException('Field '.$fieldname.' is not declared as lang field but is used in multilang context'); $type = $definition['fields'][$fieldname]['type']; } diff --git a/classes/Country.php b/classes/Country.php index 3607c47a4..6a88bef37 100644 --- a/classes/Country.php +++ b/classes/Country.php @@ -86,6 +86,10 @@ class CountryCore extends ObjectModel // Lang fields 'name' => array('type' => self::TYPE_STRING, 'lang' => true, 'validate' => 'isGenericName', 'required' => true, 'size' => 64), ), + 'associations' => array( + 'zone' => array('type' => self::HAS_ONE), + 'currency' => array('type' => self::HAS_ONE), + ) ); protected $webserviceParameters = array(