diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c811c59 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Oscar Otero Marzoa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SimpleCrud/Entity.php b/SimpleCrud/Entity.php deleted file mode 100644 index a7a26b8..0000000 --- a/SimpleCrud/Entity.php +++ /dev/null @@ -1,647 +0,0 @@ -manager = $manager; - $this->name = $name; - } - - - - /** - * init callback - */ - public function init () {} - - - - /** - * Returns an array with the fields names - * - * @return array in the format [name => name] - */ - public function getFieldsNames () { - if (!isset($this->fieldsInfo['names'])) { - $keys = array_keys($this->fields); - $this->fieldsInfo['names'] = array_combine($keys, $keys); - } - - return $this->fieldsInfo['names']; - } - - - - /** - * Returns an array with the fields defaults values of all fields - * - * @return array in the format [name => value] - */ - public function getDefaults () { - if (!isset($this->fieldsInfo['defaults'])) { - $this->fieldsInfo['defaults'] = array_fill_keys($this->getFieldsNames(), null); - } - - return $this->fieldsInfo['defaults']; - } - - - - /** - * Returns an array with the fields names ready for select queries - * - * @param array $filter Fields names to retrieve. - * - * @return array in the format [name => escapedName] - */ - public function getEscapedFieldsForSelect (array $filter = null) { - if (!isset($this->fieldsInfo['select'])) { - $this->fieldsInfo['select'] = []; - - foreach ($this->fields as $name => $field) { - $this->fieldsInfo['select'][$name] = $field->getEscapedNameForSelect(); - } - } - - if ($filter === null) { - return $this->fieldsInfo['select']; - } - - return array_intersect_key($this->fieldsInfo['select'], array_flip($filter)); - } - - - - /** - * Returns an array with the fields names ready for join queries - * - * @param array $filter Fields names to retrieve. - * - * @return array in the format [name => escapedName] - */ - public function getEscapedFieldsForJoin (array $filter = null) { - if (!isset($this->fieldsInfo['join'])) { - $this->fieldsInfo['join'] = []; - - foreach ($this->fields as $name => $field) { - $this->fieldsInfo['join'][$name] = $field->getEscapedNameForJoin(); - } - } - - if ($filter === null) { - return $this->fieldsInfo['join']; - } - - return array_intersect_key($this->fieldsInfo['join'], array_flip($filter)); - } - - - - /** - * Create a row instance from the result of a select query - * - * @param array $row The selected values - * @param boolean $expand True to expand the results (used if the select has joins) - * - * @return SimpleCrud\Row - */ - public function createFromSelection (array $row, $expand = false) { - foreach ($row as $key => &$value) { - if (isset($this->fields[$key])) { - $value = $this->fields[$key]->dataFromDatabase($value); - } - } - - if ($expand === false) { - return ($row = $this->dataFromDatabase($row)) ? $this->create($row) : false; - } - - $fields = $joinFields = []; - - foreach ($row as $name => $value) { - if (strpos($name, '.') === false) { - $fields[$name] = $value; - continue; - } - - list($name, $fieldName) = explode('.', $name, 2); - - if (!isset($joinFields[$name])) { - $joinFields[$name] = []; - } - - $joinFields[$name][$fieldName] = $value; - } - - if (!($row = $this->dataFromDatabase($fields))) { - return false; - } - - $row = $this->create($row); - - foreach ($joinFields as $name => $values) { - $row->$name = empty($values['id']) ? null : $this->manager->$name->createFromSelection($values); - } - - return $row; - } - - - - /** - * Creates a new row instance - * - * @param array $data The values of the row - * @param boolean $onlyDeclaredFields Set true to discard values in undeclared fields - * - * @return SimpleCrud\Row - */ - public function create (array $data = null, $onlyDeclaredFields = false) { - return new $this->rowClass($this, $data, $onlyDeclaredFields); - } - - - - /** - * Creates a new rowCollection instance - * - * @param array $rows Rows added to this collection - * - * @return SimpleCrud\RowCollection - */ - public function createCollection (array $rows = null) { - $collection = new $this->rowCollectionClass($this); - - if ($rows !== null) { - $collection->add($rows); - } - - return $collection; - } - - - - /** - * Executes a SELECT in the database - * - * @param string/array $where - * @param array $marks - * @param string/array $orderBy - * @param int/array $limit - * @param array $joins Optional entities to join - * @param array $from Extra tables used in the query - * - * @return mixed The row or rowcollection with the result or null - */ - public function select ($where = '', $marks = null, $orderBy = null, $limit = null, array $joins = null, array $from = null) { - if ($limit === 0) { - return $this->createCollection(); - } - - $fields = implode(', ', $this->getEscapedFieldsForSelect()); - $query = ''; - $load = []; - - if ($joins !== null) { - foreach ($joins as $name => $options) { - if (!is_array($options)) { - $name = $options; - $options = []; - } - - $entity = $this->manager->$name; - $relation = $this->getRelation($entity); - - if ($relation !== self::RELATION_HAS_ONE) { - throw new SimpleCrudException("The items '{$this->table}' and '{$entity->table}' are no related or cannot be joined"); - } - - $fields .= ', '.implode(', ', $entity->getEscapedFieldsForJoin()); - $on = "`{$entity->table}`.`id` = `{$this->table}`.`{$entity->foreignKey}`"; - - if (!empty($options['on'])) { - $on .= ' AND ('.$options['on'].')'; - - if (!empty($options['marks'])) { - $marks = array_replace($marks, $options['marks']); - } - } - - $query .= " LEFT JOIN `{$entity->table}` ON ($on)"; - } - } - - if ($from) { - $from[] = "`{$this->table}`"; - $from = implode(', ', $from); - } else { - $from = "`{$this->table}`"; - } - - $query = self::generateQuery("SELECT $fields FROM {$from}{$query}", $where, $orderBy, $limit); - - if ($limit === true || (isset($limit[1]) && $limit[1] === true)) { - $result = $this->fetchOne($query, $marks, (bool)$joins); - } else { - $result = $this->fetchAll($query, $marks, (bool)$joins); - } - - return $result; - } - - - - /** - * Executes a selection by id or by relation with other rows or collections - * - * @param mixed $id The id/ids, row or rowCollection used to select - * @param string/array $where - * @param array $marks - * @param string/array $orderBy - * @param int/array $limit - * @param array $joins Optional entities to join - * - * @return mixed The row or rowcollection with the result or null - */ - public function selectBy ($id, $where = null, $marks = null, $orderBy = null, $limit = null, array $joins = null, array $from = null) { - if (empty($id)) { - return is_array($id) ? $this->createCollection() : false; - } - - $where = empty($where) ? [] : (array)$where; - $marks = empty($marks) ? [] : (array)$marks; - - if ($id instanceof HasEntityInterface) { - if (!($relation = $this->getRelation($id->entity))) { - throw new SimpleCrudException("The items {$this->table} and {$id->entity->table} are no related"); - } - - if ($relation === self::RELATION_HAS_ONE) { - $ids = $id->get('id'); - $foreignKey = $id->entity->foreignKey; - $fetch = null; - } else if ($relation === self::RELATION_HAS_MANY) { - $ids = $id->get($this->foreignKey); - $foreignKey = 'id'; - $fetch = true; - } - - if (empty($ids)) { - return ($id instanceof RowCollection) ? $this->createCollection() : null; - } - - $where[] = "`{$this->table}`.`$foreignKey` IN (:id)"; - $marks[':id'] = $ids; - - if ($limit === null) { - $limit = (($id instanceof RowCollection) && $fetch) ? count($ids) : $fetch; - } - } else { - $where[] = 'id IN (:id)'; - $marks[':id'] = $id; - - if ($limit === null) { - $limit = is_array($id) ? count($id) : true; - } - } - - return $this->select($where, $marks, $orderBy, $limit, $joins, $from); - } - - - - /** - * Execute a count query in the database - * - * @param string/array $where - * @param array $marks - * @param int/array $limit - * - * @return int - */ - public function count ($where = null, $marks = null, $limit = null) { - $query = self::generateQuery("SELECT COUNT(*) FROM `{$this->table}`", $where, null, $limit); - - $statement = $this->manager->execute($query, $marks); - $result = $statement->fetch(\PDO::FETCH_NUM); - - return (int)$result[0]; - } - - - - /** - * Execute a query and return the first row found - * - * @param string $query The Mysql query to execute - * @param array $marks The marks passed to the statement - * @param boolean $expand Used to expand values of rows in JOINs - * - * @return SimpleCrud\Row or false - */ - public function fetchOne ($query, array $marks = null, $expand = false) { - if (!($statement = $this->manager->execute($query, $marks))) { - return false; - } - - $statement->setFetchMode(\PDO::FETCH_ASSOC); - - return ($row = $statement->fetch()) ? $this->createFromSelection($row, $expand) : false; - } - - - - /** - * Execute a query and return all rows found - * - * @param string $query The Mysql query to execute - * @param array $marks The marks passed to the statement - * @param boolean $expand Used to expand values in subrows on JOINs - * - * @return PDOStatement The result - */ - public function fetchAll ($query, array $marks = null, $expand = false) { - if (!($statement = $this->manager->execute($query, $marks))) { - return false; - } - - $statement->setFetchMode(\PDO::FETCH_ASSOC); - - $result = []; - - while (($row = $statement->fetch())) { - if (($row = $this->createFromSelection($row, $expand))) { - $result[] = $row; - } - } - - return $this->createCollection($result); - } - - - - /** - * Default data converter/validator from database - * - * @param array $data The values before insert to database - */ - public function dataToDatabase (array $data, $new) { - return $data; - } - - - - /** - * Default data converter from database - * - * @param array $data The database format values - */ - public function dataFromDatabase (array $data) { - return $data; - } - - - - /** - * Prepare the data before save into database (used by update and insert) - * - * @param array &$data The data to save - * @param bool $new True if it's a new value (insert) - */ - private function prepareDataToDatabase (array &$data, $new) { - if (!is_array($data = $this->dataToDatabase($data, $new))) { - throw new SimpleCrudException("Data not valid"); - } - - if (array_diff_key($data, $this->getFieldsNames())) { - throw new SimpleCrudException("Invalid fields"); - } - - //Transform data before save to database - $dbData = []; - - foreach ($data as $key => $value) { - $dbData[$key] = $this->fields[$key]->dataToDatabase($value); - } - - return $dbData; - } - - - - /** - * Removes unchanged data before save into database (used by update and insert) - * - * @param array $data The original data - * @param array $prepared The prepared data - * @param array $changedFields Array of changed fields. - */ - private function filterDataToSave (array $data, array $prepared, array $changedFields) { - $filtered = []; - - foreach ($data as $name => $value) { - if (isset($changedFields[$name]) || ($value !== $prepared[$name])) { - $filtered[$name] = $prepared[$name]; - } - } - - return $filtered; - } - - - - /** - * Executes an 'insert' query in the database - * - * @param array $data The values to insert - * @param boolean $duplicateKey Set true if you can avoid duplicate key errors - * - * @return array The new values of the inserted row - */ - public function insert (array $data, $duplicateKey = false) { - $preparedData = $this->prepareDataToDatabase($data, true); - - unset($preparedData['id']); - - if (empty($preparedData)) { - $query = "INSERT INTO `{$this->table}` (`id`) VALUES (NULL)"; - $marks = null; - } else { - $fields = array_keys($preparedData); - - $query = 'INSERT INTO `'.$this->table.'` (`'.implode('`, `', $fields).'`) VALUES (:'.implode(', :', $fields).')'; - $marks = []; - - foreach ($preparedData as $key => $value) { - $marks[":$key"] = $value; - } - - if ($duplicateKey) { - $query .= ' ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)'; - - foreach ($fields as $name) { - $query .= ", {$name} = :{$name}"; - } - } - - } - - $data['id'] = $this->manager->executeTransaction(function () use ($query, $marks) { - $this->manager->execute($query, $marks); - - return $this->manager->lastInsertId(); - }); - - return $data; - } - - - - /** - * Executes an 'update' query in the database - * - * @param array $data The values to update - * @param string/array $where - * @param array $marks - * @param int/array $limit - * - * @return array The new values of the updated row - */ - public function update (array $data, $where = null, $marks = null, $limit = null, array $changedFields = null) { - $originalData = $data; - $preparedData = $this->prepareDataToDatabase($data, true); - - if ($changedFields !== null) { - $preparedData = $this->filterDataToSave($originalData, $preparedData, $changedFields); - } - - unset($originalData, $preparedData['id']); - - if (empty($preparedData)) { - return $data; - } - - $query = []; - $marks = $marks ?: []; - - foreach ($preparedData as $key => $value) { - $marks[":__{$key}"] = $value; - $query[] = "`{$key}` = :__{$key}"; - } - - $query = implode(', ', $query); - $query = self::generateQuery("UPDATE `{$this->table}` SET {$query}", $where, null, $limit); - - $this->manager->executeTransaction(function () use ($query, $marks) { - $this->manager->execute($query, $marks); - }); - - return $data; - } - - - - /** - * Execute a delete query in the database - * - * @param string/array $where - * @param array $marks - * @param int/array $limit - */ - public function delete ($where = '', $marks = null, $limit = null) { - $query = self::generateQuery("DELETE FROM `{$this->table}`", $where, null, $limit); - - $this->manager->executeTransaction(function () use ($query, $marks) { - $this->manager->execute($query, $marks); - }); - } - - - - /** - * Check if this entity is related with other - * - * @param SimpleCrud\Entity / string $entity The entity object or name - * - * @return boolean - */ - public function isRelated ($entity) { - if (!($entity instanceof Entity) && !($entity = $this->manager->$entity)) { - return false; - } - - return ($this->getRelation($entity) !== null); - } - - - - /** - * Returns the relation type of this entity with other - * - * @param SimpleCrud\Entity $entity - * - * @return int One of the RELATION_* constants values or null - */ - public function getRelation (Entity $entity) { - if (isset($entity->fields[$this->foreignKey])) { - return self::RELATION_HAS_MANY; - } - - if (isset($this->fields[$entity->foreignKey])) { - return self::RELATION_HAS_ONE; - } - } -} diff --git a/SimpleCrud/EntityFactory.php b/SimpleCrud/EntityFactory.php deleted file mode 100644 index e564e1c..0000000 --- a/SimpleCrud/EntityFactory.php +++ /dev/null @@ -1,155 +0,0 @@ -entityNamespace = isset($config['namespace']) ? $config['namespace'] : ''; - - if ($this->entityNamespace && (substr($this->entityNamespace, -1) !== '\\')) { - $this->entityNamespace .= '\\'; - } - - $this->fieldsNamespace = $this->entityNamespace.'Fields\\'; - $this->autocreate = isset($config['autocreate']) ? (bool)$config['autocreate'] : false; - } - - - /** - * Creates a new instance of an Entity - * - * @param SimpleCrud\Manager $manager The manager related with this entity - * @param string $name The name of the entity - * - * @return SimpleCrud\Entity The created entity - */ - public function create (Manager $manager, $name) { - $class = $this->entityNamespace.ucfirst($name); - - if (!class_exists($class)) { - if (!$this->autocreate || !in_array($name, $this->getTables($manager))) { - return false; - } - - $class = 'SimpleCrud\\Entity'; - } - - $entity = new $class($manager, $name); - - //Configure the entity - if (empty($entity->table)) { - $entity->table = $name; - } - - if (empty($entity->foreignKey)) { - $entity->foreignKey = "{$entity->table}_id"; - } - - $entity->rowClass = class_exists("{$class}Row") ? "{$class}Row" : 'SimpleCrud\\Row'; - $entity->rowCollectionClass = class_exists("{$class}RowCollection") ? "{$class}RowCollection" : 'SimpleCrud\\RowCollection'; - - //Define fields - $fields = []; - - if (empty($entity->fields)) { - foreach ($this->getFields($manager, $entity->table) as $name => $type) { - $fields[$name] = $this->createField($entity, $name, $type); - } - } else { - foreach ($entity->fields as $name => $type) { - if (is_int($name)) { - $fields[$type] = $this->createField($entity, $type, 'field'); - } else { - $fields[$name] = $this->createField($entity, $name, $type); - } - } - } - - $entity->fields = $fields; - - //Init callback - $entity->init(); - - return $entity; - } - - - /** - * Creates a field instance - * - * @param SimpleCrud\Entity $entity The entity of the field - * @param string $name The field name - * @param string $type The field type - * - * @return SimpleCrud\Fieds\Field The created field - */ - private function createField (Entity $entity, $name, $type) { - $class = $this->fieldsNamespace.ucfirst($type); - - if (!class_exists($class)) { - $class = 'SimpleCrud\\Fields\\'.ucfirst($type); - - if (!class_exists($class)) { - $class = 'SimpleCrud\\Fields\\Field'; - } - } - - return new $class($entity, $name); - } - - - /** - * Returns a list of all fields in a table - * - * @param SimpleCrud\Manager $manager The database manager - * @param string $table The table name - * - * @return array The fields [name => type] - */ - private function getFields (Manager $manager, $table) { - $fields = []; - - foreach ($manager->execute("DESCRIBE `$table`")->fetchAll() as $field) { - preg_match('#^(\w+)#', $field['Type'], $matches); - - $fields[$field['Field']] = $matches[1]; - } - - return $fields; - } - - - /** - * Returns all tables of the database - * - * @param SimpleCrud\Manager $manager The database manager - * - * @return array The table names - */ - private function getTables (Manager $manager) { - if ($this->tables !== null) { - return $this->tables; - } - - return $this->tables = $manager->execute("SHOW TABLES")->fetchAll(\PDO::FETCH_COLUMN, 0); - } - - - /** - * Refresh the tables cache (useful after insert or drop tables in the database) - */ - public function clearCache () { - $this->tables = null; - } -} diff --git a/SimpleCrud/EntityGenerator.php b/SimpleCrud/EntityGenerator.php deleted file mode 100644 index 42f78ee..0000000 --- a/SimpleCrud/EntityGenerator.php +++ /dev/null @@ -1,103 +0,0 @@ -pdo = $pdo; - $this->path = $path; - $this->namespace = $namespace; - - if (!is_dir($this->path)) { - mkdir($this->path, 0777, true); - } - } - - - /** - * Returns all tables of the database - * - * @return array The table names - */ - private function getTables () { - return $this->pdo->query('SHOW TABLES', \PDO::FETCH_COLUMN, 0)->fetchAll(); - } - - - /** - * Returns a list of all fields in a table - * - * @param string $table The table name - * - * @return array The fields info - */ - private function getFields ($table) { - return $this->pdo->query("DESCRIBE `$table`")->fetchAll(); - } - - - public function generate () { - foreach ($this->getTables() as $table) { - $this->generateEntity($table); - } - } - - public function generateEntity ($table) { - $namespace = $this->namespace ? "namespace {$this->namespace};\n" : ''; - $className = str_replace(' ', '', ucwords(str_replace('_', ' ', $table))); - $fields = ''; - - foreach ($this->getFields($table) as $field) { - preg_match('#^(\w+)#', $field['Type'], $matches); - - $fields .= "\n\t\t'".$field['Field']."' => '".(class_exists('SimpleCrud\\Fields\\'.ucfirst($matches[1])) ? $matches[1] : 'field')."',"; - } - - $code = <<path}/{$className}.php", $code); - } -} - - - -class Json extends \SimpleCrud\Fields\Field { - public function dataToDatabase ($data) { - if (is_string($data)) { - return $data; - } - - return json_encode($data); - } - - public function dataFromDatabase ($data) { - if ($data) { - return json_decode($data, true); - } - } -} \ No newline at end of file diff --git a/SimpleCrud/Fields/Date.php b/SimpleCrud/Fields/Date.php deleted file mode 100644 index 620f857..0000000 --- a/SimpleCrud/Fields/Date.php +++ /dev/null @@ -1,20 +0,0 @@ -format('Y-m-d'); - } - } -} diff --git a/SimpleCrud/Fields/Datetime.php b/SimpleCrud/Fields/Datetime.php deleted file mode 100644 index 77ff32f..0000000 --- a/SimpleCrud/Fields/Datetime.php +++ /dev/null @@ -1,20 +0,0 @@ -format('Y-m-d H:i:s'); - } - } -} diff --git a/SimpleCrud/Fields/Field.php b/SimpleCrud/Fields/Field.php deleted file mode 100644 index 14ef7f1..0000000 --- a/SimpleCrud/Fields/Field.php +++ /dev/null @@ -1,38 +0,0 @@ -entityName = $entity->name; - $this->table = $entity->table; - $this->name = $name; - } - - final public function getEscapedNameForSelect () { - return "`{$this->table}`.`{$this->name}`"; - } - - final public function getEscapedNameForJoin () { - return "`{$this->table}`.`{$this->name}` as `{$this->entityName}.{$this->name}`"; - } - - public function dataToDatabase ($data) { - return $data; - } - - public function dataFromDatabase ($data) { - return $data; - } -} \ No newline at end of file diff --git a/SimpleCrud/Fields/Set.php b/SimpleCrud/Fields/Set.php deleted file mode 100644 index 8a7f6e6..0000000 --- a/SimpleCrud/Fields/Set.php +++ /dev/null @@ -1,22 +0,0 @@ - true]); - } - - $this->entityFactory = $entityFactory; - $this->connection = $connection; - $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } - - - /** - * Magic method to initialize the entities in lazy mode - * - * @param string $name The entity name - * - * @return SimpleCrud\Entity - */ - public function __get ($name) { - if (isset($this->entities[$name])) { - return $this->entities[$name]; - } - - $entities = $this->entityFactory->create($this, $name); - - if ($entities) { - $this->entities[$name] = $this->entityFactory->create($this, $name); - } - - return $entities; - } - - - /** - * Remove all entity instances. - * Useful when database scheme changes have made (create/drop tables or fields) - */ - public function refreshEntities () { - $this->entityFactory->clearCache(); - $this->entities = []; - } - - - /** - * Execute a query and returns the statement object with the result - * - * @param string $query The Mysql query to execute - * @param array $marks The marks passed to the statement - * - * @throws Exception On error preparing or executing the statement - * - * @return PDOStatement The result - */ - public function execute ($query, array $marks = null) { - $query = (string)$query; - - if (!empty($marks)) { - foreach ($marks as $name => $mark) { - if (is_array($mark)) { - foreach ($mark as &$val) { - $val = $this->connection->quote($val); - } - - $query = str_replace($name, implode(', ', $mark), $query); - unset($marks[$name]); - } - } - if (empty($marks)) { - $marks = null; - } - } - - try { - $statement = $this->connection->prepare($query); - $statement->execute($marks); - } catch (\Exception $exception) { - if (is_array($this->debug)) { - $this->debug[] = [ - 'error' => $exception, - 'statement' => $statement, - 'marks' => $marks - ]; - } - - throw $exception; - } - - if (is_array($this->debug)) { - $this->debug[] = [ - 'statement' => $statement, - 'marks' => $marks, - 'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20) - ]; - } - - return $statement; - } - - - /** - * Execute a callable inside a transaction - * - * @param callable $callable The function with all operations - * - * @return mixed The callable returned value - */ - public function executeTransaction (callable $callable) { - try { - $transaction = $this->beginTransaction(); - - $return = $callable(); - - if ($transaction) { - $this->commit(); - } - } catch (\Exception $exception) { - if ($transaction) { - $this->rollBack(); - } - - throw $exception; - } - - return $return; - } - - - /** - * Returns the last insert id - * - * @return int - */ - public function lastInsertId () { - return $this->connection->lastInsertId(); - } - - - /** - * Starts a transaction if it's not started yet - * - * @return boolean True if a the transaction is started or false if its not started - */ - public function beginTransaction () { - if (($this->inTransaction === false) && ($this->connection->inTransaction() === false)) { - $this->connection->beginTransaction(); - return $this->inTransaction = true; - } - - return false; - } - - - /** - * Commits the changes of the transaction to the database - */ - public function commit () { - if (($this->inTransaction === true) && ($this->connection->inTransaction() === true)) { - $this->connection->commit(); - $this->inTransaction = false; - } - } - - - /** - * rollBack a transaction - */ - public function rollBack () { - if (($this->inTransaction === true) && ($this->connection->inTransaction() === true)) { - $this->connection->rollBack(); - $this->inTransaction = false; - } - } - - - /** - * Enable, disable, get debugs - * - * @param bool Enable or disable. Null to get the debug result - * - * @return array or null - */ - public function debug ($enabled = null) { - $debug = $this->debug; - - if ($enabled === true) { - $this->debug = is_array($this->debug) ? $this->debug : []; - } else if ($enabled === false) { - $this->debug = false; - } - - return $debug; - } -} diff --git a/SimpleCrud/Row.php b/SimpleCrud/Row.php deleted file mode 100644 index 3568e2c..0000000 --- a/SimpleCrud/Row.php +++ /dev/null @@ -1,287 +0,0 @@ -entity = $entity; - $this->manager = $entity->manager; - - if ($data) { - if ($onlyDeclaredFields === true) { - $data = array_intersect_key($data, $this->entity->getFieldsNames()); - } - - $this->values = $data; - } - - $this->values += $entity->getDefaults(); - } - - - /** - * Magic method to execute 'get' functions and save the result in a property. - * - * @param string $name The property name - */ - public function __get ($name) { - $method = "get$name"; - - if (array_key_exists($name, $this->values)) { - return $this->values[$name]; - } - - if (method_exists($this, $method)) { - return $this->values[$name] = $this->$method(); - } - - if ($this->entity->isRelated($name)) { - $fn = "get$name"; - - return $this->values[$name] = $this->$fn(); - } - - return null; - } - - - /** - * Magic method to execute 'set' function - * - * @param string $name The property name - * @param mixed $value The value - */ - public function __set ($name, $value) { - $this->changes[$name] = true; - $this->values[$name] = $value; - } - - - /** - * Magic method to check if a property is defined or is empty - * - * @param string $name Property name - * - * @return boolean - */ - public function __isset ($name) { - return !empty($this->values[$name]); - } - - - /** - * Magic method to execute get[whatever] and load automatically related stuff - * - * @param string $name - * @param string $arguments - */ - public function __call ($name, $arguments) { - if ((strpos($name, 'get') === 0) && ($name = lcfirst(substr($name, 3)))) { - if (!$arguments && array_key_exists($name, $this->values)) { - return $this->values[$name]; - } - - if (($entity = $this->manager->$name)) { - array_unshift($arguments, $this); - - return call_user_func_array([$entity, 'selectBy'], $arguments); - } - } - - throw new SimpleCrudException("The method $name does not exists"); - } - - - /** - * Magic method to print the row values (and subvalues) - * - * @return string - */ - public function __toString () { - return "\n".$this->entity->name.":\n".print_r($this->toArray(), true)."\n"; - } - - - /** - * jsonSerialize interface - * - * @return array - */ - public function jsonSerialize () { - return $this->toArray(); - } - - - /** - * Return whether the row values has been changed or not - */ - public function changed () { - return !empty($this->changes); - } - - - /** - * Reload the row from the database - */ - public function reload () { - if (!$this->id || !($row = $this->entity->selectBy($this->id))) { - throw new SimpleCrudException("This row does not exist in database"); - } - - $this->changes = []; - $this->values = $row->get(); - - return $this; - } - - - /** - * Relate 'has-one' elements with this row - * - * @param HasEntityInterface $row The row to relate - * - * @return $this - */ - public function setRelation (HasEntityInterface $row) { - if (func_num_args() > 1) { - foreach (func_get_args() as $row) { - $this->setRelation($row); - } - - return $this; - } - - if ($this->entity->getRelation($row->entity) !== Entity::RELATION_HAS_ONE) { - throw new SimpleCrudException("Not valid relation"); - } - - if (empty($row->id)) { - throw new SimpleCrudException('Rows without id value cannot be related'); - } - - $this->{$row->entity->foreignKey} = $row->id; - - return $this; - } - - - /** - * Generate an array with all values and subvalues of the row - * - * @param array $parentEntities Parent entities of this row. It's used to avoid infinite recursions - * - * @return array - */ - public function toArray (array $parentEntities = array()) { - if ($parentEntities && in_array($this->entity->name, $parentEntities)) { - return null; - } - - $parentEntities[] = $this->entity->name; - $data = $this->values; - - foreach ($data as &$value) { - if ($value instanceof HasEntityInterface) { - $value = $value->toArray($parentEntities); - } - } - - return $data; - } - - - /** - * Set new values to the row. - * - * @param array $data The new values - * @param boolean $onlyDeclaredFields Set true to only set declared fields - * - * @return $this - */ - public function set (array $data, $onlyDeclaredFields = false) { - if ($onlyDeclaredFields === true) { - $data = array_intersect_key($data, $this->entity->getFieldsNames()); - } - - foreach ($data as $name => $value) { - $this->changes[$name] = true; - $this->values[$name] = $value; - } - - return $this; - } - - - /** - * Return one or all values of the row - * - * @param string $name The value name to recover. If it's not defined, returns all values. If it's true, returns only the fields values. - * - * @return mixed The value or an array with all values - */ - public function get ($name = null, $onlyChangedValues = false) { - $values = ($onlyChangedValues === true) ? array_intersect_key($this->values, $this->changes) : $this->values; - - if ($name === true) { - return array_intersect_key($values, $this->entity->getFieldsNames()); - } - - if ($name === null) { - return $values; - } - - return isset($values[$name]) ? $values[$name] : null; - } - - - /** - * Saves this row in the database - * - * @param boolean $duplicateKey Set true to detect duplicates index - * @param boolean $onlyChangedValues Set false to save all values instead only the changed (only for updates) - * - * @return $this - */ - public function save ($duplicateKey = false, $onlyChangedValues = true) { - $data = $this->get(true); - - if (empty($this->id)) { - $data = $this->entity->insert($data, $duplicateKey); - } else { - $data = $this->entity->update($data, 'id = :id', [':id' => $this->id], 1, ($onlyChangedValues ? $this->changes : null)); - } - - $this->set($data); - $this->changes = []; - - return $this; - } - - - /** - * Deletes the row in the database - * - * @return $this - */ - public function delete () { - if (empty($this->id)) { - return false; - } - - $this->entity->delete('id = :id', [':id' => $this->id], 1); - - return $this; - } -} diff --git a/SimpleCrud/RowCollection.php b/SimpleCrud/RowCollection.php deleted file mode 100644 index 1aef58b..0000000 --- a/SimpleCrud/RowCollection.php +++ /dev/null @@ -1,373 +0,0 @@ -entity = $entity; - $this->manager = $entity->manager; - } - - - /** - * Magic method to execute the get method on access to undefined property - * - * @see SimpleCrud\RowCollection::get() - */ - public function __get ($name) { - return $this->get($name); - } - - - /** - * Magic method to print the row values (and subvalues) - * - * @return string - */ - public function __toString () { - return "\n".$this->entity->name.":\n".print_r($this->toArray(), true)."\n"; - } - - - /** - * ArrayAcces interface - */ - public function offsetSet ($offset, $value) { - if (!($value instanceof Row)) { - throw new SimpleCrudException('Only instances of SimpleCrud\\Row must be added to collections'); - } - - if (!($offset = $value->id)) { - throw new SimpleCrudException('Only rows with the defined id must be added to collections'); - } - - $this->rows[$offset] = $value; - } - - - /** - * ArrayAcces interface - */ - public function offsetExists ($offset) { - return isset($this->rows[$offset]); - } - - - /** - * ArrayAcces interface - */ - public function offsetUnset ($offset) { - unset($this->rows[$offset]); - } - - - /** - * ArrayAcces interface - */ - public function offsetGet ($offset) { - return isset($this->rows[$offset]) ? $this->rows[$offset] : null; - } - - - /** - * Iterator interface - */ - public function rewind () { - return reset($this->rows); - } - - - /** - * Iterator interface - */ - public function current () { - return current($this->rows); - } - - - /** - * Iterator interface - */ - public function key () { - return key($this->rows); - } - - - /** - * Iterator interface - */ - public function next () { - return next($this->rows); - } - - - /** - * Iterator interface - */ - public function valid () { - return key($this->rows) !== null; - } - - - /** - * Countable interface - */ - public function count () { - return count($this->rows); - } - - - /** - * Magic method to execute the same function in all rows - * @param string $name The function name - * @param string $args Array with all arguments passed to the function - * - * @return $this - */ - public function __call ($name, $args) { - foreach ($this->rows as $row) { - call_user_func_array([$row, $name], $args); - } - - return $this; - } - - - /** - * jsonSerialize interface - * - * @return array - */ - public function jsonSerialize () { - return $this->toArray(); - } - - - /** - * Generate an array with all values and subvalues of the collection - * - * @param array $parentEntities Parent entities of this row. It's used to avoid infinite recursions - * - * @return array - */ - public function toArray (array $parentEntities = array()) { - if ($parentEntities && in_array($this->entity->name, $parentEntities)) { - return null; - } - - $rows = []; - - foreach ($this->rows as $row) { - $rows[] = $row->toArray($parentEntities); - } - - return $rows; - } - - - /** - * Returns one or all values of the collections - * - * @param string $name The value name. If it's not defined returns all values - * @param string $key The parameter name used for the keys. If it's not defined, returns a numerica array - * - * @return array All values found. It generates a RowCollection if the values are rows. - */ - public function get ($name = null, $key = null) { - if (is_int($name)) { - if ($key === true) { - return current(array_slice($this->rows, $name, 1)); - } - - return array_slice($this->rows, $name, $key, true); - } - - $rows = []; - - if ($name === null) { - if ($key === null) { - return array_values($this->rows); - } - - foreach ($this->rows as $row) { - $k = $row->$key; - - if (!empty($k)) { - $rows[$k] = $row; - } - } - - return $rows; - } - - if ($key !== null) { - foreach ($this->rows as $row) { - $k = $row->$key; - - if (!empty($k)) { - $rows[$k] = $row->$name; - } - } - - return $rows; - } - - foreach ($this->rows as $row) { - $value = $row->$name; - - if (!empty($value)) { - $rows[] = $value; - } - } - - if ($this->entity->isRelated($name)) { - $entity = $this->manager->$name; - $collection = $entity->createCollection(); - - if ($this->entity->getRelation($entity) === Entity::RELATION_HAS_ONE) { - $collection->add($rows); - } else { - foreach ($rows as $rows) { - $collection->add($rows); - } - } - - return $collection; - } - - return $rows; - } - - - /** - * Add new values to the collection - * - * @param array/HasEntityInterface $rows The new rows - */ - public function add ($rows) { - if (is_array($rows) || ($rows instanceof RowCollection)) { - foreach ($rows as $row) { - $this->offsetSet(null, $row); - } - } else if (isset($rows)) { - $this->offsetSet(null, $rows); - } - - return $this; - } - - - /** - * Filter the rows by a value - * - * @param string $name The value name - * @param mixed $value The value to filter - * @param boolean $first Set true to return only the first row found - * - * @return SimpleCrud\HasEntityInterface The rows found or null if no value is found and $first parameter is true - */ - public function filter ($name, $value, $first = false) { - $rows = []; - - foreach ($this->rows as $row) { - if (($row->$name === $value) || (is_array($value) && in_array($row->$name, $value, true))) { - if ($first === true) { - return $row; - } - - $rows[] = $row; - } - } - - return $first ? null : $this->entity->createCollection($rows); - } - - - /** - * Load related elements from the database - * - * @param array $entities The entities names - * - * @return $this - */ - public function load ($entity) { - if (!($entity = $this->manager->$entity)) { - throw new SimpleCrudException("The entity $entity does not exists"); - } - - $arguments[0] = $this; - $result = call_user_func_array([$entity, 'selectBy'], $arguments); - - $this->distribute($result); - - return $this; - } - - - /** - * Distribute a row or rowcollection througth all rows - * - * @param SimpleCrud\HasEntityInterface $data The row or rowcollection to distribute - * @param boolean $bidirectional Set true to distribute also in reverse direccion - * - * @return $this - */ - public function distribute (HasEntityInterface $data, $bidirectional = true) { - if ($data instanceof Row) { - $data = $data->entity->createCollection([$data]); - } - - if ($data instanceof RowCollection) { - $name = $data->entity->name; - - switch ($this->entity->getRelation($data->entity)) { - case Entity::RELATION_HAS_MANY: - $foreignKey = $this->entity->foreignKey; - - foreach ($this->rows as $row) { - if (!isset($row->$name)) { - $row->$name = $data->entity->createCollection(); - } - } - - foreach ($data as $row) { - $id = $row->$foreignKey; - - if (isset($this->rows[$id])) { - $this->rows[$id]->$name->add($row); - } - } - - if ($bidirectional === true) { - $data->distribute($this, false); - } - - return $this; - - case Entity::RELATION_HAS_ONE: - $foreignKey = $data->entity->foreignKey; - - foreach ($this->rows as $row) { - $row->$name = (($id = $row->$foreignKey) && isset($data[$id])) ? $data[$id] : null; - } - - if ($bidirectional === true) { - $data->distribute($this, false); - } - - return $this; - } - - throw new SimpleCrudException("Cannot set '$name' and '{$this->entity->name}' because is not related or does not exists"); - } - } -} diff --git a/SimpleCrud/SimpleCrudException.php b/SimpleCrud/SimpleCrudException.php deleted file mode 100644 index 1d616e5..0000000 --- a/SimpleCrud/SimpleCrudException.php +++ /dev/null @@ -1,8 +0,0 @@ -=5.4" }, "autoload": { - "psr-0": { - "SimpleCrud": "" + "psr-4": { + "SimpleCrud\\": "src" } } } \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9d776d1 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + ./tests/ + + + diff --git a/src/Adapters/Adapter.php b/src/Adapters/Adapter.php new file mode 100644 index 0000000..d414d56 --- /dev/null +++ b/src/Adapters/Adapter.php @@ -0,0 +1,168 @@ + true]); + } + + $this->entityFactory = $entityFactory; + $this->connection = $connection; + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + /** + * Magic method to initialize the entities in lazy mode. + * + * @param string $name The entity name + * + * @return null|Entity + */ + public function __get($name) + { + if (isset($this->entities[$name])) { + return $this->entities[$name]; + } + + $entities = $this->entityFactory->create($this, $name); + + if ($entities) { + $this->entities[$name] = $this->entityFactory->create($this, $name); + } + + return $entities; + } + + /** + * Execute a query and returns the statement object with the result. + * + * @param string $query The Mysql query to execute + * @param array $marks The marks passed to the statement + * + * @throws Exception On error preparing or executing the statement + * + * @return PDOStatement The result + */ + public function execute($query, array $marks = null) + { + $query = (string) $query; + + if (!empty($marks)) { + foreach ($marks as $name => $mark) { + if (is_array($mark)) { + foreach ($mark as &$val) { + $val = $this->connection->quote($val); + } + + $query = str_replace($name, implode(', ', $mark), $query); + unset($marks[$name]); + } + } + if (empty($marks)) { + $marks = null; + } + } + + $statement = $this->connection->prepare($query); + $statement->execute($marks); + + return $statement; + } + + /** + * Execute a callable inside a transaction. + * + * @param callable $callable The function with all operations + * + * @return mixed The callable returned value + */ + public function executeTransaction(callable $callable) + { + try { + $transaction = $this->beginTransaction(); + + $return = $callable(); + + if ($transaction) { + $this->commit(); + } + } catch (\Exception $exception) { + if ($transaction) { + $this->rollBack(); + } + + throw $exception; + } + + return $return; + } + + /** + * Returns the last insert id. + * + * @return integer + */ + public function lastInsertId() + { + return $this->connection->lastInsertId(); + } + + /** + * Starts a transaction if it's not started yet. + * + * @return boolean True if a the transaction is started or false if its not started + */ + public function beginTransaction() + { + if (($this->inTransaction === false) && ($this->connection->inTransaction() === false)) { + $this->connection->beginTransaction(); + + return $this->inTransaction = true; + } + + return false; + } + + /** + * Commits the changes of the transaction to the database. + */ + public function commit() + { + if (($this->inTransaction === true) && ($this->connection->inTransaction() === true)) { + $this->connection->commit(); + $this->inTransaction = false; + } + } + + /** + * RollBack a transaction. + */ + public function rollBack() + { + if (($this->inTransaction === true) && ($this->connection->inTransaction() === true)) { + $this->connection->rollBack(); + $this->inTransaction = false; + } + } +} diff --git a/src/Adapters/AdapterInterface.php b/src/Adapters/AdapterInterface.php new file mode 100644 index 0000000..2395efc --- /dev/null +++ b/src/Adapters/AdapterInterface.php @@ -0,0 +1,128 @@ + type] + */ + public function getFields($table); + + /** + * Returns all tables of the database. + * + * @return array + */ + public function getTables(); +} diff --git a/src/Adapters/MySql.php b/src/Adapters/MySql.php new file mode 100644 index 0000000..6f88ff8 --- /dev/null +++ b/src/Adapters/MySql.php @@ -0,0 +1,293 @@ +execute(implode(' ', $query), $marks); + } + + /** + * {@inheritdoc} + */ + public function count($table, $where = null, array $marks = null, $limit = null) + { + $query = ['SELECT COUNT(*)']; + $query[] = "FROM `{$table}`"; + + if (($where = static::generateWhere($where)) !== null) { + $query[] = $where; + } + + if (($limit = static::generateLimit($limit)) !== null) { + $query[] = $limit; + } + + $statement = $this->execute(implode(' ', $query), $marks); + $result = $statement->fetch(\PDO::FETCH_NUM); + + return (int)$result[0]; + } + + /** + * {@inheritdoc} + */ + public function insert($table, array $data = null, $duplicateKeyErrors = false); + { + if (empty($data)) { + return "INSERT INTO `{$table}` (`id`) VALUES (NULL)"; + } + + $fields = array_keys($data); + + $query = ["INSERT INTO `{$table}`"]; + $query[] = '(`'.implode('`, `', $fields).'`)'; + $query[] = 'VALUES'; + $query[] = '(:'.implode(', :', $fields).')'; + + if (!$duplicateKeyErrors) { + $query[] = 'ON DUPLICATE KEY UPDATE'; + $query[] = 'id = LAST_INSERT_ID(id), '.static::generateUpdateFields($fields); + } + + $marks = []; + + foreach ($data as $field => $value) { + $marks[":{$field}"] = $value; + } + + return $this->execute(implode(' ', $query), $marks); + } + + /** + * {@inheritdoc} + */ + public function update($table, array $data, $where = null, array $marks = null, $limit = null) + { + $query = ["UPDATE `{$table}`"]; + $query[] = 'SET '.static::generateUpdateFields(array_keys($data), '__'); + + if (($where = static::generateWhere($where)) !== null) { + $query[] = $where; + } + + if (($limit = static::generateLimit($limit)) !== null) { + $query[] = $limit; + } + + $marks = $marks ?: []; + + foreach ($data as $field => $value) { + $marks[":__{$field}"] = $value; + } + + return $this->execute(implode(' ', $query), $marks); + } + + /** + * {@inheritdoc} + */ + public function delete($table, $where = null, array $marks = null, $limit = null) + { + $query = ["DELETE FROM `{$table}`"]; + + if (($where = static::generateWhere($where)) !== null) { + $query[] = $where; + } + + if (($limit = static::generateLimit($limit)) !== null) { + $query[] = $limit; + } + + $this->execute(implode(' ', $query), $marks); + } + + /** + * {@inheritdoc} + */ + public function getFields ($table) { + $fields = []; + + foreach ($this->execute("DESCRIBE `{$table}`")->fetchAll() as $field) { + preg_match('#^(\w+)#', $field['Type'], $matches); + + $fields[$field['Field']] = $matches[1]; + } + + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getTables () { + return $this->execute('SHOW TABLES')->fetchAll(\PDO::FETCH_COLUMN, 0); + } + + /** + * Generates the code for the fields in an update/insert query + * + * @param array $fields + * @param string $markPrefix + * + * @return string + */ + protected static function generateUpdateFields(array $fields, $markPrefix = '') + { + $update = []; + + foreach ($fields as $name) { + $update[] = "`{$name}` = :{$markPrefix}{$name}"; + } + + return implode(', ', $update); + } + + /** + * Generates the fields/tables part of a SELECT query + * + * @param array $selectFields + * @param array|null $joins + * + * @return string + */ + protected static function generateSelect(array $selectFields, array $joins = null) + { + $escapedFields = []; + $escapedTables = []; + + foreach ($selectFields as $table => $fields) { + $escapedTables[] = "`{$table}`"; + + foreach ($fields as $field) { + $escapedFields[] = "`{$table}`.`{$field}`"; + } + } + + if (!empty($joins)) { + foreach ($joins as $join) { + if (!isset($join['fields'])) { + continue; + } + + foreach ($join['fields'] as $field) { + $escapedFields[] "`{$join['table']}`.`{$field}` as `{$join['name']}.{$field}`"; + } + } + } + + return implode(',', $escapedFields).' FROM '.implode(',', $escapedTables); + } + + /** + * Generate a LEFT JOIN clause + * + * @param mixed $joins + * + * @return string|null + */ + protected static function generateJoins($joins) + { + if (empty($joins)) { + return; + } + + $escapedJoins = []; + + foreach ($joins as $join) { + $currentJoin = ['LEFT JOIN']; + $currentJoin[] = "`{$join['table']}`"; + + if (!empty($join['on'])) { + $currentJoin[] = static::generateWhere($join['on'], 'ON'); + } + + $escapedJoins[] = $currentJoin; + } + + return implode(' ', $escapedJoins); + } + + /** + * Generate a WHERE clause + * + * @param mixed $where + * + * @return string|null + */ + protected static function generateWhere($where, $clause = 'WHERE') + { + if (empty($where)) { + return; + } + + if (is_array($where)) { + $where = implode(') AND (', $where); + } + + return "{$clause} ($where)"; + } + + /** + * Generate an ORDER BY clause + * + * @param mixed $orderBy + * + * @return string|null + */ + protected static function generateOrderBy($orderBy) + { + if (empty($orderBy)) { + return; + } + + return 'ORDER BY '.(is_array($orderBy) ? implode(', ', $orderBy) : $orderBy); + } + + /** + * Generate a LIMIT clause + * + * @param mixed $limit + * + * @return string|null + */ + protected static function generateLimit($limit) + { + if (empty($limit)) { + return; + } + + if ($limit === true) { + return 'LIMIT 1'; + } + + return 'LIMIT '.(is_array($limit) ? implode(', ', $limit) : $limit); + } +} diff --git a/src/Entity.php b/src/Entity.php new file mode 100644 index 0000000..e0ae5d5 --- /dev/null +++ b/src/Entity.php @@ -0,0 +1,521 @@ +adapter = $adapter; + $this->name = $name; + } + + /** + * init callback. + */ + public function init() + { + } + + /** + * Returns an array with the defaults values. + * + * @return array + */ + public function getDefaults() + { + return array_fill_keys(array_keys($this->fields), null); + } + + /** + * Create a row instance from the result of a select query. + * + * @param array $row The selected values + * @param boolean $expand True to expand the results (used if the select has joins) + * + * @return false|Row + */ + public function createFromSelection(array $row, $expand = false) + { + foreach ($row as $key => &$value) { + if (isset($this->fields[$key])) { + $value = $this->fields[$key]->dataFromDatabase($value); + } + } + + if ($expand === false) { + return ($row = $this->dataFromDatabase($row)) ? $this->create($row) : false; + } + + $fields = $joinFields = []; + + foreach ($row as $name => $value) { + if (strpos($name, '.') === false) { + $fields[$name] = $value; + continue; + } + + list($name, $fieldName) = explode('.', $name, 2); + + if (!isset($joinFields[$name])) { + $joinFields[$name] = []; + } + + $joinFields[$name][$fieldName] = $value; + } + + if (!($row = $this->dataFromDatabase($fields))) { + return false; + } + + $row = $this->create($row); + + foreach ($joinFields as $name => $values) { + $row->$name = empty($values['id']) ? null : $this->manager->$name->createFromSelection($values); + } + + return $row; + } + + /** + * Creates a new row instance. + * + * @param array $data The values of the row + * @param boolean $onlyDeclaredFields Set true to discard values in undeclared fields + * + * @return Row + */ + public function create(array $data = null, $onlyDeclaredFields = false) + { + if (!empty($data) && $onlyDeclaredFields === true) { + $data = array_intersect_key($data, $this->fields); + } + + return new $this->rowClass($this, $data); + } + + /** + * Creates a new rowCollection instance. + * + * @param array $rows Rows added to this collection + * + * @return RowCollection + */ + public function createCollection(array $rows = null) + { + $collection = new $this->rowCollectionClass($this); + + if ($rows !== null) { + $collection->add($rows); + } + + return $collection; + } + + /** + * Executes a SELECT in the database. + * + * @param string/array $where + * @param null|array $marks + * @param string/array $orderBy + * @param int/array $limit + * @param null|array $joins Optional entities to join + * @param null|array $from Extra tables used in the query + * + * @return mixed The row or rowcollection with the result or null + */ + public function select($where = '', array $marks = null, $orderBy = null, $limit = null, array $joins = null, array $from = null) + { + if ($limit === 0) { + return $this->createCollection(); + } + + $selectFields = [ + $this->table => $this->getFields(), + ]; + + if ($from) { + foreach ($from as $table) { + $selectFields[$table] = []; + } + } + + $selectJoins = []; + + if ($joins !== null) { + foreach ($joins as $name => $options) { + if (!is_array($options)) { + $name = $options; + $options = []; + } + + $entity = $this->manager->$name; + $relation = $this->getRelation($entity); + + if ($relation !== self::RELATION_HAS_ONE) { + throw new SimpleCrudException("The items '{$this->table}' and '{$entity->table}' are no related or cannot be joined"); + } + $currentJoin = [ + 'table' => $entity->table, + 'name' => $entity->name, + 'fields' => $entity->getFields(), + 'on' => ["`{$entity->table}`.`id` = `{$this->table}`.`{$entity->foreignKey}`"], + ]; + + if (!empty($options['on'])) { + $currentJoin['on'][] = $options['on']; + + if (!empty($options['marks'])) { + $marks = array_replace($marks, $options['marks']); + } + } + + $selectJoins[] = $currentJoin; + } + } + + $statement = $this->adapter->executeSelect($selectFields, $selectJoins, $where, $marks, $orderBy, $limit); + + if ($limit === true || (isset($limit[1]) && $limit[1] === true)) { + return $this->createFromStatement($statement); + } + + return $this->createCollectionFromStatement($statement); + } + + /** + * Executes a selection by id or by relation with other rows or collections. + * + * @param mixed $id The id/ids, row or rowCollection used to select + * @param string/array $where + * @param array $marks + * @param string/array $orderBy + * @param int/array $limit + * @param array $joins Optional entities to join + * + * @return mixed The row or rowcollection with the result or null + */ + public function selectBy($id, $where = null, $marks = null, $orderBy = null, $limit = null, array $joins = null, array $from = null) + { + if (empty($id)) { + return is_array($id) ? $this->createCollection() : false; + } + + $where = empty($where) ? [] : (array) $where; + $marks = empty($marks) ? [] : (array) $marks; + + if ($id instanceof RowInterface) { + if (!($relation = $this->getRelation($id->entity))) { + throw new SimpleCrudException("The items {$this->table} and {$id->entity->table} are no related"); + } + + if ($relation === self::RELATION_HAS_ONE) { + $ids = $id->get('id'); + $foreignKey = $id->entity->foreignKey; + $fetch = null; + } elseif ($relation === self::RELATION_HAS_MANY) { + $ids = $id->get($this->foreignKey); + $foreignKey = 'id'; + $fetch = true; + } + + if (empty($ids)) { + return ($id instanceof RowCollection) ? $this->createCollection() : null; + } + + $where[] = "`{$this->table}`.`$foreignKey` IN (:id)"; + $marks[':id'] = $ids; + + if ($limit === null) { + $limit = (($id instanceof RowCollection) && $fetch) ? count($ids) : $fetch; + } + } else { + $where[] = 'id IN (:id)'; + $marks[':id'] = $id; + + if ($limit === null) { + $limit = is_array($id) ? count($id) : true; + } + } + + return $this->select($where, $marks, $orderBy, $limit, $joins, $from); + } + + /** + * Execute a count query in the database. + * + * @param string/array $where + * @param array $marks + * @param int/array $limit + * + * @return int + */ + public function count($where = null, $marks = null, $limit = null) + { + $query = $this->queryBuilder->count($this->table, $where, $limit); + $statement = $this->manager->execute($query, $marks); + $result = $statement->fetch(\PDO::FETCH_NUM); + + return (int) $result[0]; + } + + /** + * Execute a query and return the first row found. + * + * @param PDOStatement $statement + * @param boolean $expand Used to expand values of rows in JOINs + * + * @return null|Row + */ + public function createFromStatement(PDOStatement $statement, $expand = false) + { + $statement->setFetchMode(PDO::FETCH_ASSOC); + + if (($data = $statement->fetch())) { + return $this->createFromSelection($data, $expand); + } + } + + /** + * Execute a query and return the first row found. + * + * @param PDOStatement $statement + * @param boolean $expand Used to expand values of rows in JOINs + * + * @return RowCollection + */ + public function createCollectionFromStatement(PDOStatement $statement, $expand = false) + { + $statement->setFetchMode(PDO::FETCH_ASSOC); + + $result = []; + + while (($row = $statement->fetch())) { + if (($row = $this->createFromSelection($row, $expand))) { + $result[] = $row; + } + } + + return $this->createCollection($result); + } + + /** + * Execute a query and return the first row found. + * + * @param string $query The Mysql query to execute or the statement with the result + * @param array $marks The marks passed to the statement + * @param boolean $expand Used to expand values of rows in JOINs + * + * @return null|Row + */ + public function fetchOne($query, array $marks = null, $expand = false) + { + return $this->createFromStatement($this->adapter->execute($query, $marks), $expand); + } + + /** + * Execute a query and return all rows found. + * + * @param string $query The Mysql query to execute + * @param array $marks The marks passed to the statement + * @param boolean $expand Used to expand values in subrows on JOINs + * + * @return RowCollection + */ + public function fetchAll($query, array $marks = null, $expand = false) + { + return $this->createCollectionFromStatement($this->adapter->execute($query, $marks), $expand); + } + + /** + * Default data converter/validator from database. + * + * @param array $data The values before insert to database + */ + public function dataToDatabase(array $data, $new) + { + return $data; + } + + /** + * Default data converter from database. + * + * @param array $data The database format values + */ + public function dataFromDatabase(array $data) + { + return $data; + } + + /** + * Prepare the data before save into database (used by update and insert). + * + * @param array &$data The data to save + * @param bool $new True if it's a new value (insert) + */ + private function prepareDataToDatabase(array &$data, $new) + { + if (!is_array($data = $this->dataToDatabase($data, $new))) { + throw new SimpleCrudException("Data not valid"); + } + + if (array_diff_key($data, $this->getFieldsNames())) { + throw new SimpleCrudException("Invalid fields"); + } + + //Transform data before save to database + $dbData = []; + + foreach ($data as $key => $value) { + $dbData[$key] = $this->fields[$key]->dataToDatabase($value); + } + + return $dbData; + } + + /** + * Removes unchanged data before save into database (used by update and insert). + * + * @param array $data The original data + * @param array $prepared The prepared data + * @param array $changedFields Array of changed fields. + */ + private function filterDataToSave(array $data, array $prepared, array $changedFields) + { + $filtered = []; + + foreach ($data as $name => $value) { + if (isset($changedFields[$name]) || ($value !== $prepared[$name])) { + $filtered[$name] = $prepared[$name]; + } + } + + return $filtered; + } + + /** + * Executes an 'insert' query in the database. + * + * @param array $data The values to insert + * @param boolean $duplicateKey Set true if you can avoid duplicate key errors + * + * @return array The new values of the inserted row + */ + public function insert(array $data, $duplicateKey = false) + { + $preparedData = $this->prepareDataToDatabase($data, true); + + unset($preparedData['id']); + + $data['id'] = $this->adapter->executeTransaction(function () use ($preparedData, $duplicateKey) { + $this->adapter->insert($this->table, $preparedData, $duplicateKey); + + return $this->adapter->lastInsertId(); + }); + + return $data; + } + + /** + * Executes an 'update' query in the database. + * + * @param array $data The values to update + * @param string/array $where + * @param array $marks + * @param int/array $limit + * + * @return array The new values of the updated row + */ + public function update(array $data, $where = null, $marks = null, $limit = null, array $changedFields = null) + { + $originalData = $data; + $preparedData = $this->prepareDataToDatabase($data, true); + + if ($changedFields !== null) { + $preparedData = $this->filterDataToSave($originalData, $preparedData, $changedFields); + } + + unset($originalData, $preparedData['id']); + + if (empty($preparedData)) { + return $data; + } + + $this->adapter->executeTransaction(function () use ($preparedData, $where, $marks, $limit) { + $this->adapter->update($this->table, $preparedData, $where, $marks, $limit); + }); + + return $data; + } + + /** + * Execute a delete query in the database. + * + * @param string/array $where + * @param array $marks + * @param int/array $limit + */ + public function delete($where = null, $marks = null, $limit = null) + { + $this->adapter->executeTransaction(function () use ($where, $marks, $limit) { + $this->adapter->delete($this->table, $where, $marks, $limit); + }); + } + + /** + * Check if this entity is related with other. + * + * @param SimpleCrud\Entity / string $entity The entity object or name + * + * @return boolean + */ + public function isRelated($entity) + { + if (!($entity instanceof Entity) && !($entity = $this->manager->$entity)) { + return false; + } + + return ($this->getRelation($entity) !== null); + } + + /** + * Returns the relation type of this entity with other. + * + * @param SimpleCrud\Entity $entity + * + * @return int One of the RELATION_* constants values or null + */ + public function getRelation(Entity $entity) + { + if (isset($entity->fields[$this->foreignKey])) { + return self::RELATION_HAS_MANY; + } + + if (isset($this->fields[$entity->foreignKey])) { + return self::RELATION_HAS_ONE; + } + } +} diff --git a/src/EntityFactory.php b/src/EntityFactory.php new file mode 100644 index 0000000..16ae441 --- /dev/null +++ b/src/EntityFactory.php @@ -0,0 +1,114 @@ +entityNamespace = isset($config['namespace']) ? $config['namespace'] : ''; + $this->fieldsNamespace = $this->entityNamespace.'Fields\\'; + $this->autocreate = isset($config['autocreate']) ? (bool) $config['autocreate'] : false; + } + + public function setAdapter(Adapter $adapter) + { + $this->adapter = $adapter; + } + + /** + * Creates a new instance of an Entity. + * + * @param string $name + * + * @return Entity + */ + public function create($name) + { + $class = $this->entityNamespace.ucfirst($name); + + if (!class_exists($class)) { + if ($this->autocreate) { + if ($this->tables === null) { + $this->tables = $this->adapter->getTables(); + } + + if (!in_array($name, $this->tables)) { + return false; + } + } + + $class = 'SimpleCrud\\Entity'; + } + + $entity = new $class($this->adapter, $name); + + //Configure the entity + if (empty($entity->table)) { + $entity->table = $name; + } + + if (empty($entity->foreignKey)) { + $entity->foreignKey = "{$entity->table}_id"; + } + + $entity->rowClass = class_exists("{$class}Row") ? "{$class}Row" : 'SimpleCrud\\Row'; + $entity->rowCollectionClass = class_exists("{$class}RowCollection") ? "{$class}RowCollection" : 'SimpleCrud\\RowCollection'; + + //Define fields + $fields = []; + + if (empty($entity->fields)) { + foreach ($this->adapter->getFields($entity->table) as $name => $type) { + $fields[$name] = $this->createField($type); + } + } else { + foreach ($entity->fields as $name => $type) { + if (is_int($name)) { + $fields[$type] = $this->createField('field'); + } else { + $fields[$name] = $this->createField($type); + } + } + } + + $entity->fields = $fields; + + //Init callback + $entity->init(); + + return $entity; + } + + /** + * Creates a field instance. + * + * @param string $type The field type + * + * @return FieldInterface The created field + */ + private function createField($type) + { + $class = $this->fieldsNamespace.ucfirst($type); + + if (!class_exists($class)) { + $class = 'SimpleCrud\\Fields\\'.ucfirst($type); + + if (!class_exists($class)) { + $class = 'SimpleCrud\\Fields\\Field'; + } + } + + return new $class(); + } +} diff --git a/src/EntityGenerator.php b/src/EntityGenerator.php new file mode 100644 index 0000000..911762c --- /dev/null +++ b/src/EntityGenerator.php @@ -0,0 +1,106 @@ +pdo = $pdo; + $this->path = $path; + $this->namespace = $namespace; + + if (!is_dir($this->path)) { + mkdir($this->path, 0777, true); + } + } + + /** + * Returns all tables of the database. + * + * @return array The table names + */ + private function getTables() + { + return $this->pdo->query('SHOW TABLES', \PDO::FETCH_COLUMN, 0)->fetchAll(); + } + + /** + * Returns a list of all fields in a table. + * + * @param string $table The table name + * + * @return array The fields info + */ + private function getFields($table) + { + return $this->pdo->query("DESCRIBE `$table`")->fetchAll(); + } + + public function generate() + { + foreach ($this->getTables() as $table) { + $this->generateEntity($table); + } + } + + public function generateEntity($table) + { + $namespace = $this->namespace ? "namespace {$this->namespace};\n" : ''; + $className = str_replace(' ', '', ucwords(str_replace('_', ' ', $table))); + $fields = ''; + + foreach ($this->getFields($table) as $field) { + preg_match('#^(\w+)#', $field['Type'], $matches); + + $fields .= "\n\t\t'".$field['Field']."' => '".(class_exists('SimpleCrud\\Fields\\'.ucfirst($matches[1])) ? $matches[1] : 'field')."',"; + } + + $code = <<path}/{$className}.php", $code); + } +} + +class Json extends \SimpleCrud\Fields\Field +{ + public function dataToDatabase($data) + { + if (is_string($data)) { + return $data; + } + + return json_encode($data); + } + + public function dataFromDatabase($data) + { + if ($data) { + return json_decode($data, true); + } + } +} diff --git a/src/Fields/Date.php b/src/Fields/Date.php new file mode 100644 index 0000000..d9cfba9 --- /dev/null +++ b/src/Fields/Date.php @@ -0,0 +1,10 @@ +format, strtotime($data)); + } + + if ($data instanceof \Datetime) { + return $data->format($this->format); + } + } + + /** + * {@inheritdoc} + */ + public function dataFromDatabase($data) + { + return $data; + } +} diff --git a/src/Fields/Field.php b/src/Fields/Field.php new file mode 100644 index 0000000..96552f2 --- /dev/null +++ b/src/Fields/Field.php @@ -0,0 +1,24 @@ +entity = $entity; + $this->adapter = $entity->adapter; + + if ($data) { + $this->values = $data; + } + + $this->values += $entity->getDefaults(); + } + + /** + * Magic method to execute 'get' functions and save the result in a property. + * + * @param string $name The property name + */ + public function __get($name) + { + $method = "get$name"; + + if (array_key_exists($name, $this->values)) { + return $this->values[$name]; + } + + if (method_exists($this, $method)) { + return $this->values[$name] = $this->$method(); + } + + if ($this->entity->isRelated($name)) { + $fn = "get$name"; + + return $this->values[$name] = $this->$fn(); + } + } + + /** + * Magic method to execute 'set' function. + * + * @param string $name The property name + * @param mixed $value The value + */ + public function __set($name, $value) + { + $this->changes[$name] = true; + $this->values[$name] = $value; + } + + /** + * Magic method to check if a property is defined or is empty. + * + * @param string $name Property name + * + * @return boolean + */ + public function __isset($name) + { + return !empty($this->values[$name]); + } + + /** + * Magic method to execute get[whatever] and load automatically related stuff. + * + * @param string $name + * @param string $arguments + * + * @throws SimpleCrudException + */ + public function __call($name, $arguments) + { + if ((strpos($name, 'get') === 0) && ($name = lcfirst(substr($name, 3)))) { + if (!$arguments && array_key_exists($name, $this->values)) { + return $this->values[$name]; + } + + if (($entity = $this->adapter->$name)) { + array_unshift($arguments, $this); + + return call_user_func_array([$entity, 'selectBy'], $arguments); + } + } + + throw new SimpleCrudException("The method $name does not exists"); + } + + /** + * Magic method to print the row values (and subvalues). + * + * @return string + */ + public function __toString() + { + return "\n".$this->entity->name.":\n".print_r($this->toArray(), true)."\n"; + } + + /** + * jsonSerialize interface. + * + * @return array + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * Return if the row values has been changed or not. + * + * @return boolean + */ + public function changed() + { + return !empty($this->changes); + } + + /** + * Reload the row from the database. + * + * @throws SimpleCrudException + * + * @return $this + */ + public function reload() + { + if (!$this->id || !($row = $this->entity->selectBy($this->id))) { + throw new SimpleCrudException("This row does not exist in database"); + } + + $this->changes = []; + $this->values = $row->get(); + + return $this; + } + + /** + * Relate 'has-one' elements with this row. + * + * @param RowInterface $row The row to relate + * + * @return $this + */ + public function setRelation(RowInterface $row) + { + if (func_num_args() > 1) { + foreach (func_get_args() as $row) { + $this->setRelation($row); + } + + return $this; + } + + if ($this->entity->getRelation($row->entity) !== Entity::RELATION_HAS_ONE) { + throw new SimpleCrudException("Not valid relation"); + } + + if (empty($row->id)) { + throw new SimpleCrudException('Rows without id value cannot be related'); + } + + $this->{$row->entity->foreignKey} = $row->id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function toArray($keysAsId = false, array $parentEntities = array()) + { + if ($parentEntities && in_array($this->entity->name, $parentEntities)) { + return; + } + + $parentEntities[] = $this->entity->name; + $data = $this->values; + + foreach ($data as &$value) { + if ($value instanceof RowInterface) { + $value = $value->toArray($keysAsId, $parentEntities); + } + } + + return $data; + } + + /** + * Set new values to the row. + * + * @param array $data The new values + * @param boolean $onlyDeclaredFields Set true to only set declared fields + * + * @return $this + */ + public function set(array $data, $onlyDeclaredFields = false) + { + if ($onlyDeclaredFields === true) { + $data = array_intersect_key($data, $this->entity->getFieldsNames()); + } + + foreach ($data as $name => $value) { + $this->changes[$name] = true; + $this->values[$name] = $value; + } + + return $this; + } + + /** + * Return one or all values of the row. + * + * @param true|null|string $name The value name to recover. If it's not defined, returns all values. If it's true, returns only the fields values. + * + * @return mixed + */ + public function get($name = null, $onlyChangedValues = false) + { + $values = ($onlyChangedValues === true) ? array_intersect_key($this->values, $this->changes) : $this->values; + + if ($name === true) { + return array_intersect_key($values, $this->entity->getFieldsNames()); + } + + if ($name === null) { + return $values; + } + + return isset($values[$name]) ? $values[$name] : null; + } + + /** + * Saves this row in the database. + * + * @param boolean $duplicateKey Set true to detect duplicates index + * @param boolean $onlyChangedValues Set false to save all values instead only the changed (only for updates) + * + * @return $this + */ + public function save($duplicateKey = false, $onlyChangedValues = true) + { + $data = $this->get(true); + + if (empty($this->id)) { + $data = $this->entity->insert($data, $duplicateKey); + } else { + $data = $this->entity->update($data, 'id = :id', [':id' => $this->id], 1, ($onlyChangedValues ? $this->changes : null)); + } + + $this->set($data); + $this->changes = []; + + return $this; + } + + /** + * Deletes the row in the database. + * + * @return $this + */ + public function delete() + { + if (empty($this->id)) { + return false; + } + + $this->entity->delete('id = :id', [':id' => $this->id], 1); + + return $this; + } +} diff --git a/src/RowCollection.php b/src/RowCollection.php new file mode 100644 index 0000000..00d4526 --- /dev/null +++ b/src/RowCollection.php @@ -0,0 +1,377 @@ +entity = $entity; + $this->adapter = $entity->adapter; + } + + /** + * Magic method to execute the get method on access to undefined property. + * + * @see RowCollection::get() + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Magic method to print the row values (and subvalues). + * + * @return string + */ + public function __toString() + { + return "\n".$this->entity->name.":\n".print_r($this->toArray(), true)."\n"; + } + + /** + * @see ArrayAccess + */ + public function offsetSet($offset, $value) + { + if (!($value instanceof Row)) { + throw new SimpleCrudException('Only instances of SimpleCrud\\Row must be added to collections'); + } + + if (!($offset = $value->id)) { + throw new SimpleCrudException('Only rows with the defined id must be added to collections'); + } + + $this->rows[$offset] = $value; + } + + /** + * @see ArrayAccess + */ + public function offsetExists($offset) + { + return isset($this->rows[$offset]); + } + + /** + * @see ArrayAccess + */ + public function offsetUnset($offset) + { + unset($this->rows[$offset]); + } + + /** + * @see ArrayAccess + */ + public function offsetGet($offset) + { + return isset($this->rows[$offset]) ? $this->rows[$offset] : null; + } + + /** + * @see Iterator + */ + public function rewind() + { + return reset($this->rows); + } + + /** + * @see Iterator + */ + public function current() + { + return current($this->rows); + } + + /** + * @see Iterator + */ + public function key() + { + return key($this->rows); + } + + /** + * @see Iterator + */ + public function next() + { + return next($this->rows); + } + + /** + * @see Iterator + */ + public function valid() + { + return key($this->rows) !== null; + } + + /** + * @see Countable + */ + public function count() + { + return count($this->rows); + } + + /** + * Magic method to execute the same function in all rows. + * + * @param string $name The function name + * @param string $args Array with all arguments passed to the function + * + * @return $this + */ + public function __call($name, $args) + { + foreach ($this->rows as $row) { + call_user_func_array([$row, $name], $args); + } + + return $this; + } + + /** + * @see JsonSerialize + */ + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * {@inheritdoc} + */ + public function toArray($keysAsId = false, array $parentEntities = array()) + { + if ($parentEntities && in_array($this->entity->name, $parentEntities)) { + return; + } + + $rows = []; + + foreach ($this->rows as $id => $row) { + $rows[$id] = $row->toArray($parentEntities); + } + + return $keysAsId ? $rows : array_values($rows); + } + + /** + * Returns one or all values of the collections. + * + * @param string $name The value name. If it's not defined returns all values + * @param string $key The parameter name used for the keys. If it's not defined, returns a numeric array + * + * @return array|RowCollection All values found. It generates a RowCollection if the values are rows. + */ + public function get($name = null, $key = null) + { + if (is_int($name)) { + if ($key === true) { + return current(array_slice($this->rows, $name, 1)); + } + + return array_slice($this->rows, $name, $key, true); + } + + $rows = []; + + if ($name === null) { + if ($key === null) { + return array_values($this->rows); + } + + foreach ($this->rows as $row) { + $k = $row->$key; + + if (!empty($k)) { + $rows[$k] = $row; + } + } + + return $rows; + } + + if ($key !== null) { + foreach ($this->rows as $row) { + $k = $row->$key; + + if (!empty($k)) { + $rows[$k] = $row->$name; + } + } + + return $rows; + } + + foreach ($this->rows as $row) { + $value = $row->$name; + + if (!empty($value)) { + $rows[] = $value; + } + } + + if ($this->entity->isRelated($name)) { + $entity = $this->adapter->$name; + $collection = $entity->createCollection(); + + if ($this->entity->getRelation($entity) === Entity::RELATION_HAS_ONE) { + $collection->add($rows); + } else { + foreach ($rows as $rows) { + $collection->add($rows); + } + } + + return $collection; + } + + return $rows; + } + + /** + * Add new values to the collection. + * + * @param array|RowInterface $rows The new rows + * + * @return $this + */ + public function add($rows) + { + if (is_array($rows) || ($rows instanceof RowCollection)) { + foreach ($rows as $row) { + $this->offsetSet(null, $row); + } + } elseif (isset($rows)) { + $this->offsetSet(null, $rows); + } + + return $this; + } + + /** + * Filter the rows by a value. + * + * @param string $name The value name + * @param mixed $value The value to filter + * @param boolean $first Set true to return only the first row found + * + * @return null|RowInterface The rows found or null if no value is found and $first parameter is true + */ + public function filter($name, $value, $first = false) + { + $rows = []; + + foreach ($this->rows as $row) { + if (($row->$name === $value) || (is_array($value) && in_array($row->$name, $value, true))) { + if ($first === true) { + return $row; + } + + $rows[] = $row; + } + } + + return $first ? null : $this->entity->createCollection($rows); + } + + /** + * Load related elements from the database. + * + * @param string $entity The entity name + * + * @return $this + */ + public function load($entity) + { + if (!($entity = $this->adapter->$entity)) { + throw new SimpleCrudException("The entity $entity does not exists"); + } + + $arguments[0] = $this; + $result = call_user_func_array([$entity, 'selectBy'], $arguments); + + $this->distribute($result); + + return $this; + } + + /** + * Distribute a row or rowcollection througth all rows. + * + * @param RowInterface $data The row or rowcollection to distribute + * @param boolean $bidirectional Set true to distribute also in reverse direccion + * + * @return $this + */ + public function distribute(RowInterface $data, $bidirectional = true) + { + if ($data instanceof Row) { + $data = $data->entity->createCollection([$data]); + } + + if ($data instanceof RowCollection) { + $name = $data->entity->name; + + switch ($this->entity->getRelation($data->entity)) { + case Entity::RELATION_HAS_MANY: + $foreignKey = $this->entity->foreignKey; + + foreach ($this->rows as $row) { + if (!isset($row->$name)) { + $row->$name = $data->entity->createCollection(); + } + } + + foreach ($data as $row) { + $id = $row->$foreignKey; + + if (isset($this->rows[$id])) { + $this->rows[$id]->$name->add($row); + } + } + + if ($bidirectional === true) { + $data->distribute($this, false); + } + + return $this; + + case Entity::RELATION_HAS_ONE: + $foreignKey = $data->entity->foreignKey; + + foreach ($this->rows as $row) { + $row->$name = (($id = $row->$foreignKey) && isset($data[$id])) ? $data[$id] : null; + } + + if ($bidirectional === true) { + $data->distribute($this, false); + } + + return $this; + } + + throw new SimpleCrudException("Cannot set '$name' and '{$this->entity->name}' because is not related or does not exists"); + } + } +} diff --git a/src/RowInterface.php b/src/RowInterface.php new file mode 100644 index 0000000..ee7ddf5 --- /dev/null +++ b/src/RowInterface.php @@ -0,0 +1,18 @@ +