* * Este programa é software livre; você pode redistribuí-lo e/ou modificá-lo * sob os termos da Licença Pública Geral GNU conforme publicada pela Free * Software Foundation; tanto a versão 2 da Licença, como (a seu critério) * qualquer versão posterior. * * Este programa é distribuí­do na expectativa de que seja útil, porém, SEM * NENHUMA GARANTIA; nem mesmo a garantia implí­cita de COMERCIABILIDADE OU * ADEQUAÇÃO A UMA FINALIDADE ESPECÍFICA. Consulte a Licença Pública Geral * do GNU para mais detalhes. * * Você deve ter recebido uma cópia da Licença Pública Geral do GNU junto * com este programa; se não, escreva para a Free Software Foundation, Inc., no * endereço 59 Temple Street, Suite 330, Boston, MA 02111-1307 USA. * * @author Eriksen Costa Paixão * @category i-Educar * @license @@license@@ * @package CoreExt_DataMapper * @since Arquivo disponível desde a versão 1.1.0 * @version $Id$ */ /** * CoreExt_DataMapper abstract class. * * Implementa alguns dos conceitos do pattern Data Mapper de forma simples. A * intenção é o de tornar o mapeamento objeto-relacional mais simples, * permitindo a criação de objetos de domínio novos que interajam com objetos * de domínio legados. * * @author Eriksen Costa Paixão * @category i-Educar * @license @@license@@ * @package CoreExt_DataMapper * @since Classe disponível desde a versão 1.1.0 * @todo Refactoring dos métodos get*Statment() com extract method * @todo Testes para ordenação em findAll() * @version @@package_version@@ */ abstract class CoreExt_DataMapper { /** * Classe CoreExt_Entity para este data mapper. * @var string */ protected $_entityClass = ''; /** * Mapeamento objeto-relacional, atributos-campos. * @var array */ protected $_attributeMap = array(); /** * Atributos não-persistíveis. * @var array */ protected $_notPersistable = array(); /** * Define as chaves primárias da tabela. Configurada automaticamente para * usar o campo identidade de CoreExt_Entity. * @see CoreExt_Entity::_createIdentityField() * @var array */ protected $_primaryKey = array('id'); /** * Objeto de conexão com o banco de dados. * @var clsBanco */ protected $_dbAdapter = NULL; /** * Instância padrão para uso em objetos CoreExt_DataMapper. Isso possibilita * que a mesma instância do adapter de conexão com o banco de dados seja * reutilizado em várias instâncias de CoreExt_DataMapper. * * @var clsBanco */ protected static $_defaultDbAdapter = NULL; /** * Nome da tabela em que o objeto é mapeado. * @var string */ protected $_tableName = ''; /** * Nome do schema da tabela. * @var string */ protected $_tableSchema = ''; /** * @var CoreExt_Locale */ protected $_locale = NULL; /** * Construtor. * @param clsBanco $db */ public function __construct(clsBanco $db = NULL) { if (!is_null($db)) { $this->_setDbAdapter($db); } } /** * Setter para configuração de um adapter de banco de dados padrão usado * nas instâncias concretas de CoreExt_DataMapper quando nenhuma instância de * clsBanco é passada ao construtor. * @param clsBanco $db */ public static function setDefaultDbAdapter(clsBanco $db = NULL) { self::$_defaultDbAdapter = $db; } /** * Reseta o adapter padrão, fazendo com que CoreExt_DataMapper instancie * automaticamente uma instância de clsBanco quando necessário. */ public static function resetDefaultDbAdapter() { self::setDefaultDbAdapter(NULL); } /** * Setter para o objeto de adapter responsável pela interação com o banco de * dados. * * @param clsBanco $db * @return CoreExt_DataMapper Provê interface fluída */ protected function _setDbAdapter(clsBanco $db) { $this->_dbAdapter = $db; return $this; } /** * Getter para o objeto de adapter de banco de dados. * * Se nenhuma instância foi explicitamente passada ao construtor, * tenta atribuir uma instância por padrão, na seguinte ordem: * * - Usando o adapter provido pelo método estático setDefaultDbAdapter * (útil para usar várias instâncias de CoreExt_DataMapper sem a instanciação * da classe clsBanco) * - Ou, instanciando a classe clsBanco * * Usar o setter estático tem a vantagem de reduzir o overhead causado pela * instanciação a clsBanco a cada novo objeto CoreExt_DataMapper. * * @return clsBanco */ protected function _getDbAdapter() { if (is_null($this->_dbAdapter)) { if (!is_null(self::$_defaultDbAdapter)) { $adapter = self::$_defaultDbAdapter; } else { $adapter = new clsBanco(array('fetchMode' => clsBanco::FETCH_ASSOC)); } $this->_setDbAdapter($adapter); } return $this->_dbAdapter; } /** * Getter público para o objeto de adapter de banco de dados. * @return clsBanco */ public function getDbAdapter() { return $this->_getDbAdapter(); } /** * Retorna o nome da tabela. Se o $_tableSchema for informado, retorna o * nome da tabela no formato 'schema.table'. * * @return string */ protected function _getTableName() { return $this->_tableSchema != '' ? $this->_tableSchema . '.' . $this->_tableName : $this->_tableName; } /** * Retorna o nome do recurso, isto é o nome da tabela sem '_', * Ex: transporte_aluno => transporte aluno. * * @return string */ public function resourceName() { return strtolower(str_replace('_', ' ', $this->_tableName)); } /** * Retorna os nomes das colunas da tabela em um array, de acordo com o array * de dados associativo $data. * * Caso nenhum array seja passado, é usado o array de atributos da classe * CoreExt_Entity ao qual o data mapper mapeia. * * @param array $data * @return array */ protected function _getTableColumnsArray(array $data = array()) { $columns = array(); if (0 == count($data)) { $tempEntity = new $this->_entityClass(); $data = $tempEntity->toDataArray(); } $tempColumns = array_map(array($this, '_getTableColumn'), array_keys($data)); // Remove colunas não-persistíveis foreach ($tempColumns as $key => $column) { if (is_null($column)) { continue; } $columns[] = $column; } return $columns; } /** * Retorna o nome do campo da tabela caso o identificador do atributo * esteja mapeado em $_attributeMap. * * Caso contrário, retorna o próprio identificador do atributo. * * @param string $key * @return string|NULL NULL para coluna não persistível */ protected function _getTableColumn($key) { if (in_array($key, $this->_notPersistable)) { return NULL; } if (array_key_exists($key, $this->_attributeMap)) { return $this->_attributeMap[$key]; } return $key; } /** * Retorna os nomes das colunas da tabela separados por vírgula e espaço (', '). * * @param array $data * @return string */ protected function _getTableColumns(array $data = array()) { return join(', ', $this->_getTableColumnsArray($data)); } /** * Retorna uma query SQL de recuperação de todos os registros de uma tabela. * * @param array $data * @param array $where * @param array $orderBy * @return string */ protected function _getFindAllStatment(array $data = array(), array $where = array(), array $orderBy = array()) { $whereArg = $where; $where = array(); $order = array(); if (0 < count($whereArg)) { foreach ($whereArg as $key => $value) { $whereName = $this->_getTableColumn($key); preg_match('/[<,=,>]/', $value, $matches); $hasComparisonSign = ! empty($matches); // Caso $value contenha <, > ou =, ex: '> $1', não adiciona sinal de igual. if($hasComparisonSign) $where[] = sprintf("%s %s", $whereName, $value); // Caso $value contenha parametros para consulta preparada ($1, $2...), não adiciona $value entre aspas. elseif(strpos($value, '$') > -1) $where[] = sprintf("%s = %s", $whereName, $value); else $where[] = sprintf("%s = '%s'", $whereName, $value); } } else { $where[] = '1 = 1'; } if (0 < count($orderBy)) { foreach ($orderBy as $key => $value) { $order[] = sprintf('%s %s', $this->_getTableColumn($key), $value); } $order = sprintf('ORDER BY %s', implode(',', $order)); } else { $order = ''; } return sprintf("SELECT %s FROM %s WHERE %s %s", $this->_getTableColumns($data), $this->_getTableName(), implode(' AND ', $where), $order); } /** * Retorna uma query SQL de recuperação de registro baseada na identidade. * * Converte o argumento $pkey para float, como forma de * evitar os problemas do tipo int em ambientes 32 bit (mais especificamente, * a ausência de um tipo long). * * @link http://php.net/manual/en/language.types.integer.php * @link http://php.net/manual/en/function.intval.php * @param array|long $pkey * @return string */ protected function _getFindStatment($pkey) { $where = array(); if (!is_array($pkey)) { $where[] = sprintf("id = '%d'", floatval($pkey)); } elseif (is_array($pkey)) { foreach ($pkey as $key => $pk) { if (is_int($key)) { $whereName = $this->_getTableColumn($this->_primaryKey[$key]); } elseif (is_string($key)) { $whereName = $this->_getTableColumn($key); } $where[] = sprintf("%s = '%d'", $whereName, floatval($pk)); } } return sprintf("SELECT %s FROM %s WHERE %s", $this->_getTableColumns(), $this->_getTableName(), implode(' AND ', $where)); } /** * Retorna uma query SQL para a operação INSERT. Utiliza para isso os * atributos da instância CoreExt_Entity, com o cuidado de remover o * campo identidade. * * Uma query gerada por esse método segue a forma: * * INSERT INTO [schema.]table (column) VALUES ('value'); * * * @param CoreExt_Entity $instance * @return string */ protected function _getSaveStatment(CoreExt_Entity $instance) { $sql = 'INSERT INTO %s (%s) VALUES (%s)'; $data = $this->_getDbAdapter()->formatValues($instance->toDataArray()); // Remove o campo identidade e campos não-persistentes $data = $this->_cleanData($data); // Pega apenas os valores do array $values = array_values($data); // Colunas para formar a query $columns = $this->_getTableColumns($data); // Trata os valores NULL diferentemente dos outros, para evitar erro // de execução query $valuesStmt = array(); for ($i = 0, $count = count($values); $i < $count; $i++) { $value = $values[$i]; if (is_null($value)) { $value = "NULL"; $replaceString = "%s"; } else { $replaceString = "'%s'"; } $valuesStmt[] = sprintf($replaceString, $value); } $valuesStmt = join(", ", $valuesStmt); return sprintf($sql, $this->_getTableName(), $columns, $valuesStmt); } /** * Retorna uma query SQL para a operação UPDATE. Utiliza para isso os * atributos da instância CoreExt_Entity, usando o atributo identidade * para especificar qual registro atualizar na tabela. * * Uma query gerada por esse método segue a forma: * * UPDATE [schema.]table SET column='value' WHERE id = 'idValue'; * * * @param CoreExt_Entity $instance * @return string */ protected function _getUpdateStatment(CoreExt_Entity $instance) { $sql = 'UPDATE %s SET %s WHERE %s'; $data = $this->_getDbAdapter()->formatValues($instance->toDataArray()); // Remove o campo identidade e campos não-persistentes $data = $this->_cleanData($data); // Trata os valores NULL diferentemente dos outros, para evitar erro // de execução query $columns = array(); foreach ($data as $key => $value) { $columnName = $this->_getTableColumn($key); if (is_null($value)) { $value = "NULL"; $replaceString = "%s = %s"; } else { $replaceString = "%s = '%s'"; } $columns[] = sprintf($replaceString, $columnName, $value); } $where = array(); foreach ($this->_primaryKey as $pk) { $whereName = $this->_getTableColumn($pk); $where[] = sprintf("%s = '%d'", $whereName, $instance->get($pk)); } return sprintf($sql, $this->_getTableName(), implode(', ', $columns), implode(' AND ', $where)); } /** * Retorna uma query SQL para a operação DELETE. Utiliza para isso o * atributo identidade "id" (caso seja passada uma instância de CoreExt_Entity * como parâmetro) ou o valor inteiro passado como parâmetro. * * Uma query gerada por esse método segue a forma: * * DELETE FROM [schema.]table WHERE id = 'idValue'; * * * @param mixed $instance * @return string */ protected function _getDeleteStatment($instance) { $sql = 'DELETE FROM %s WHERE %s'; $where = array(); if ($instance instanceof CoreExt_Entity) { foreach ($this->_primaryKey as $pk) { $whereName = $this->_getTableColumn($pk); //$where[] = sprintf("%s = '%d'", $whereName, $instance->get($pk)); estoura o decimal. valor 9801762824 retornando 1211828232 $where[] = sprintf("%s = '%s'", $whereName, $instance->get($pk)); } } elseif (is_numeric($instance)) { $where[] = sprintf("%s = '%d'", 'id', floatval($instance)); } elseif (is_array($instance)) { foreach ($this->_primaryKey as $pk) { $whereName = $this->_getTableColumn($pk); $where[] = sprintf("%s = '%d'", $whereName, $instance[$pk]); } } return sprintf($sql, $this->_getTableName(), implode(' AND ', $where)); } /** * Retorna todos os registros como objetos CoreExt_Entity retornados pela * query de _getFindAllStatment(). * * @param array $columns Atributos a serem carregados. O atributo id é sempre carregado. * @param array $where * @param array $orderBy * @param array $addColumnIdIfNotSet Se true, adiciona a coluna 'id' caso não esteja definido no array $columns * @return array * @todo Problema potencial com busca em registros com compount key. Testar. */ public function findAll(array $columns = array(), array $where = array(), array $orderBy = array(), $addColumnIdIfNotSet = true) { // Inverte chave valor, permitindo array simples como array('nome') if (0 < count($columns)) { $columns = array_flip($columns); if (!isset($columns['id']) && $addColumnIdIfNotSet) { $columns['id'] = TRUE; } } // Reseta o locale para o default (en_US) $this->getLocale()->resetLocale(); $this->_getDbAdapter()->Consulta($this->_getFindAllStatment($columns, $where, $orderBy)); $list = array(); // Retorna o locale para o usado no restante da aplicação $this->getLocale()->setLocale(); while ($this->_getDbAdapter()->ProximoRegistro()) { $list[] = $this->_createEntityObject($this->_getDbAdapter()->Tupla()); } return $list; } /** * Retorna todos os registros como objetos CoreExt_Entity retornados pela * query de _getFindAllStatment() (usando consulta preparada, util para evitar sql injection). * * @param array $columns Atributos a serem carregados. O atributo id é sempre carregado. * @param array $where Condicoes preparadas ex: array('arg1 = $1', 'arg2 = $2'); * @param array $params Valor das condiçoes ($1, $2 ...) ex: array('1', '3'); * @param array $orderBy * @param array $addColumnIdIfNotSet Se true, adiciona a coluna 'id' caso não esteja definido no array $columns * @return array * @todo */ public function findAllUsingPreparedQuery(array $columns = array(), array $where = array(), array $params = array(), array $orderBy = array(), $addColumnIdIfNotSet = true) { $list = array(); // Inverte chave valor, permitindo array simples como array('nome') if (0 < count($columns)) { $columns = array_flip($columns); if (!isset($columns['id']) && $addColumnIdIfNotSet) { $columns['id'] = TRUE; } } // Reseta o locale para o default (en_US) $this->getLocale()->resetLocale(); $sql = $this->_getFindAllStatment($columns, $where, $orderBy); if ($this->_getDbAdapter()->execPreparedQuery($sql, $params) != false) { // Retorna o locale para o usado no restante da aplicação $this->getLocale()->setLocale(); while ($this->_getDbAdapter()->ProximoRegistro()) { $list[] = $this->_createEntityObject($this->_getDbAdapter()->Tupla()); } } return $list; } /** * Retorna um registro que tenha como identificador (chave única ou composta) * o valor dado por $pkey. * * @param array|long $pkey * @return CoreExt_Entity * @throws Exception */ public function find($pkey) { $this->_getDbAdapter()->Consulta($this->_getFindStatment($pkey)); if (FALSE === $this->_getDbAdapter()->ProximoRegistro()) { $table = $this->_tableSchema . '.' . $this->_tableName; throw new Exception("Nenhum registro encontrado na tabela '$table' com a(s) chaves(s) informada(s): '$pkey'."); } return $this->_createEntityObject($this->_getDbAdapter()->Tupla()); } /** * Salva ou atualiza um registro através de uma instância de CoreExt_Entity. * * @param CoreExt_Entity $instance * @return bool * @throws CoreExt_DataMapper_Exception|Exception */ public function save(CoreExt_Entity $instance) { if (!$instance->isValid()) { throw new Exception('A instânca de "' . get_class($instance) . '" contém erros de validação.'); } // Coumpound key, todos os valores precisam estar setados, seja para // INSERT ou UPDATE. A instância precisa ser marcada explicitamente // como "old" para que UPDATE seja chamado. if (1 < count($this->_primaryKey)) { foreach ($this->_primaryKey as $pk) { $value = $instance->get($pk); if (!isset($value)) { require_once 'CoreExt/DataMapper/Exception.php'; throw new CoreExt_DataMapper_Exception('Erro de compound key. Uma das primary keys tem o valor NULL: "' . $pk . '"'); } } } // Field identity, se estiver presente, marca instância como "old". elseif (1 == count($this->_primaryKey)) { if (isset($instance->id)) { $instance->markOld(); } } // Reseta o locale para o default (en_US) $this->getLocale()->resetLocale(); if ($instance->isNew()) { return $this->_getDbAdapter()->Consulta($this->_getSaveStatment($instance)); } else { return $this->_getDbAdapter()->Consulta($this->_getUpdateStatment($instance)); } // Retorna o locale para o usado no restante da aplicação $this->getLocale()->setLocale(); } /** * Apaga um registro através de uma instância CoreExt_Entity. Pode apagar * recebendo uma instância com as chaves primárias setadas ou um array * associativo de chaves primárias e seus valores. * * Exemplo: * * 1, 'pk2' => 2)); * $dataMapper = new CoreExt_DataMapper(); * * // Por valor do campo identidade 'id' * $dataMapper->delete(1); * * // Pelas chaves primárias de uma instância * $dataMapper->delete($instance); * * // Por um array de chaves associativas de chaves primárias * $dataMapper->delete(array('pk1' => 1, 'pk2' => 2)); * * * @param mixed $instance * @return bool */ public function delete($instance) { return $this->_getDbAdapter()->Consulta($this->_getDeleteStatment($instance)); } /** * Retorna uma nova instância de CoreExt_Entity. A instância criada não * produz efeito algum no comportamento de CoreExt_DataMapper. * * @return CoreExt_Entity */ public function createNewEntityInstance(array $data = array()) { return new $this->_entityClass($data); } /** */ protected function _cleanData(array $data) { if (array_key_exists('id', $data)) { unset($data['id']); } // Remove dados não-persistíveis foreach ($this->_notPersistable as $field) { if (array_key_exists($field, $data)) { unset($data[$field]); } } return $data; } /** * Cria um objeto CoreExt_Entity com os valores dos campos relacionais * mapeados para os atributos da instância. * * @param array $data * @return CoreExt_Entity */ protected function _createEntityObject(array $data) { $instance = $this->createNewEntityInstance(); $instance->markOld(); $instance = $this->_mapData($data, $instance); return $instance; } /** * Mapeia os campos relacionais para os atributos de uma instância de * CoreExt_Entity. * * @param array $data * @param CoreExt_Entity $instance * @return CoreExt_Entity A instância com os atributos mapeados */ protected function _mapData($data, CoreExt_Entity $instance) { foreach ($data as $key => $value) { try { $instance->$key = $value; } catch (CoreExt_Exception_InvalidArgumentException $e) { // Caso o campo não tenha um atributo correspondente, procura no // mapa de atributos pelo equivalente e atribue. if (FALSE !== ($index = array_search($key, $this->_attributeMap))) { $instance->$index = $value; } } } return $instance; } /** * Setter. * @param CoreExt_Locale $instance * @return CoreExt_DataMapper Provê interface fluída */ public function setLocale(CoreExt_Locale $instance) { $this->_locale = $instance; return $this; } /** * Getter. * @return CoreExt_Locale */ public function getLocale() { if (is_null($this->_locale)) { require_once 'CoreExt/Locale.php'; $this->setLocale(CoreExt_Locale::getInstance()); } return $this->_locale; } }