<?php
use \Doctrine\ORM\EntityRepository,
\Doctrine\ORM\QueryBuilder,
\Doctrine\ORM\EntityManager,
\Doctrine\ORM\Query,
Doctrine\ORM\Query\AST,
\Doctrine\DBAL\Types\Type,
\Doctrine\ORM\Mapping\ClassMetadata;
/**
* Provides you the ability to use Doctrine as a source
* with the Grid.
*
* @package Bvb_Grid
* @author Martin Parsiegla <martin.parsiegla@speanet.info>
*/
class Bvb_Grid_Source_Doctrine2 extends Bvb_Grid_Source_Db_DbAbstract implements Bvb_Grid_Source_SourceInterface
{
/**
* @var QueryBuilder
*/
private $qb;
/**
* @var EntityManager
*/
private $entityManager;
/**
* An array containing title, type and fieldname from the query fields.
*
* @var array
*/
private $fields;
/**
* Array with all defined where conditions.
*
* @var array
*/
private $whereConditions = array();
/**
* Ascii coded alias (97 = 'a').
*
* @var integer
*/
private $alias = 97;
/**
* The parameter number to use.
*
* @var int
*/
private $paramterNumber = 1;
/**
* Class construct.
*
* @param string|QueryBuilder $value
* @param EntityManager $entityManager
*/
function __construct($value, $entityManager = null)
{
$this->setEntityManager($entityManager);
$this->setQueryBuilder($value);
}
/**
* Retriev the entity manager.
*
* @return EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
/**
* Sets the entity manager.
*
* If no entity manager is assigned, we try to get it from the registry.
*
* @param EntityManager $entityManager
*/
public function setEntityManager($entityManager = null)
{
if(is_null($entityManager)) {
if(Zend_Registry::isRegistered('doctrine')) {
$entityManager = Zend_Registry::get('doctrine')->getEntityManager();
} elseif(Zend_Registry::isRegistered('EntityManager')) {
$entityManager = Zend_Registry::get('EntityManager');
} else {
throw new Bvb_Grid_Source_Doctrine2_Exception('No suitable EntityManager found in registry, please set a specific one.');
}
} elseif(!($entityManager instanceof EntityManager)) {
throw new Bvb_Grid_Source_Doctrine2_Exception('Parameter must be an instance of \Doctrine\ORM\EntityManager');
}
$this->entityManager = $entityManager;
}
/**
* Returns the query builder.
*
* @return QueryBuilder
*/
public function getQueryBuilder()
{
return $this->qb;
}
/**
* Sets the query builder.
*
* @param string|EntityManager|EntityRepository $value
* @return Bvb_Grid_Source_Doctrine2
*/
public function setQueryBuilder($value)
{
//if the value is a string, check if the entity class
//exists and create the query builder
if(is_string($value)) {
$em = $this->getEntityManager();
//check if the class exists, surpress any warnings
if(!@class_exists($value, true)) {
throw new Bvb_Grid_Source_Doctrine2_Exception('Entity with name ' . $value . ' does not exist.');
}
$qb = $em->getRepository($value)->createQueryBuilder('d');
} elseif($value instanceof EntityRepository) {
$qb = $value->createQueryBuilder('d');
} elseif(!($value instanceof QueryBuilder)) {
throw new Bvb_Grid_Source_Doctrine2_Exception('Parameter must be an instance of \Doctrine\ORM\EntityRepository or \Doctrine\ORM\QueryBuilder.');
} else {
$qb = $value;
}
$this->qb = $qb;
return $this;
}
/**
* Adds a new condition to the current query
* $filter is the value to be filtered
* $op is the opreand to be used: =,>=, like, llike,REGEX,
* $completeField. use the index $completField['field'] to
* specify the field, to avoid ambiguous
*
* @param $filter
* @param $op
* @param $completeField
* @return Bvb_Grid_Source_Doctrine
*/
public function addCondition($filter, $op, $completeField)
{
$field = $completeField['field'];
$this->whereConditions[$field]['filter'] = $filter;
$this->whereConditions[$field]['op'] = $op;
$qb = $this->getQueryBuilder();
$this->_addCondition($qb, $field, $op, $filter);
return $this;
}
/**
* Adds a new condition to the given QueryBuilder.
*
* @param QueryBuilder $qb
* @param string $field The field to be used for the condition
* @param string $op The operator to be used for the condition.
* @param string $filter
*/
private function _addCondition(QueryBuilder $qb, $field, $op, $filter)
{
$pn = $this->_getNewParameterNumber();
$pnUse = '?' . $pn;
$func = 'where';
if(strpos($field, '(') !== false) {
$func = 'having';
}
$havingPart = $qb->getDQLPart('having');
$wherePart = $qb->getDQLPart('where');
if($func == 'having' && !empty($havingPart)) {
$func = 'andHaving';
} elseif($func == 'where' && !empty($wherePart)) {
$func = 'andWhere';
}
switch(strtolower($op)) {
case 'sqlexp':
$qb->$func($filter);
break;
case 'isnull':
$qb->$func($field . ' IS NULL ');
break;
case 'isnotnull':
$qb->$func($field . ' IS NOT NULL ');
break;
case 'empty':
$expr = $qb->expr()->eq($field, '');
$qb->$func($expr);
break;
case 'equal':
case '=':
$expr = $qb->expr()->eq($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter);
break;
case 'regex':
$qb->$func($field . " REGEXP " . $pnUse);
$qb->setParameter($pn, $filter);
break;
case 'rlike':
$expr = $qb->expr()->like($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter . "%");
break;
case 'llike':
$expr = $qb->expr()->like($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, "%" . $filter);
break;
case '>=':
$expr = $qb->expr()->gte($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter);
break;
case '>':
$expr = $qb->expr()->gt($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter);
break;
case '<>':
case '!=':
$expr = $qb->expr()->neq($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter);
break;
case '<=':
$expr = $qb->expr()->lte($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter);
break;
case '<':
$expr = $qb->expr()->lt($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, $filter);
break;
case 'in':
$filter = explode(',', $filter);
$qb->$func($qb->expr()->in($field, $filter));
break;
case '&':
case 'and':
case 'AND':
case 'flag':
case 'FLAG':
$qb->$func($field . " & " . $pnUse . " <> 0");
$qb->setParameter($pn, $filter);
break;
case 'range':
$pn2 = $this->_getNewParameterNumber();
$pn2Use = '?' . $pn2;
$expr = $qb->expr()->between($field, $pnUse, $pn2Use);
$start = substr($filter, 0, strpos($filter, '<>'));
$end = substr($filter, strpos($filter, '<>') + 2);
$qb->$func($expr);
$qb->setParameter($pn, $start);
$qb->setParameter($pn2, $end);
break;
case '||':
$expr = $qb->expr()->like($field, $pnUse);
$qb->orWhere($expr);
$qb->setParameter($pn, "%" . $filter . "%");
break;
case 'like':
default:
$expr = $qb->expr()->like($field, $pnUse);
$qb->$func($expr);
$qb->setParameter($pn, "%" . $filter . "%");
break;
}
return $this;
}
public function addFullTextSearch($filter, $field)
{
throw new Bvb_Grid_Source_Doctrine2_Exception("Fulltext searching is currently not supported by the Doctrine2 source.");
}
/**
* builds a key=>value array
*
* they must have two options
* title and field
* field is used to perform queries.
* Must have table name or table alias as a prefix
* ex: user.id | country.population
*
* The key for this array is the output field
* If raw sql is somehting like
*
* select name as alias, country from users
*
* the return array must be like this:
*
* array('alias'=>array('title'=>'alias','field'=>'users.name'));
*
* its not bad idea to apply this to fields titles
* $title = ucwords(str_replace('_',' ',$title));
*
* @return array
*/
public function buildFields()
{
if(empty($fields)) {
$ast = $this->getQueryBuilder()->getQuery()->getAST();
//used for expressions without an identification variable
$fieldNumber = 1;
$returnFields = array();
foreach($ast->selectClause->selectExpressions as $selectExpression) {
$expression = $selectExpression->expression;
//if the expression is a string, there is an alias used to get all fields
//to get all fields from the alias, we fetch the entity class and retrieve
//the metadata from it, so we can set the fields correctly
if(is_string($expression)) {
//the expression itself is a pathexpression, where we can directly
//fetch the title and field
$alias = $expression;
$tableName = $this->_getModelFromAlias($alias);
$metadata = $this->getEntityManager()->getClassMetadata($tableName);
foreach($metadata->fieldMappings as $key => $details) {
$returnFields[$key]['title'] = ucwords(str_replace('_', ' ', $key));
$returnFields[$key]['field'] = $alias . '.' . $key;
$returnFields[$key]['type'] = $details['type'];
}
} elseif($expression instanceof AST\PathExpression) {
$field = ($selectExpression->fieldIdentificationVariable != null) ? $selectExpression->fieldIdentificationVariable : $expression->field;
$returnFields[$field]['title'] = ucwords(str_replace('_', ' ', $field));
$returnFields[$field]['field'] = $expression->identificationVariable . '.' . $expression->field;
} elseif($expression instanceof AST\Subselect) {
//handle subselects. we only need the identification variable for the field
$field = $selectExpression->fieldIdentificationVariable;
$title = ucwords(str_replace('_', ' ', $field));
$returnFields[$field]['title'] = $title;
$returnFields[$field]['field'] = $field;
} else {
$field = $selectExpression->fieldIdentificationVariable;
//doctrine uses numeric keys for expressions which got no
//identification variable, so the key will be set to the
//current counter $i
if($field === null) {
$field = $this->_getNameForExpression($expression);
$key = $fieldNumber;
$fieldNumber++;
} else {
$key = $field;
}
$title = ucwords(str_replace('_', ' ', $field));
$returnFields[$key]['title'] = $title;
$returnFields[$key]['field'] = $field;
}
}
$this->fields = $returnFields;
}
return $this->fields;
}
/**
* Generates the expression used in the select expression
*
* @param FunctionNode $expression
* @return string
*/
private function _getNameForExpression($expression)
{
$str = '';
foreach($expression as $key => $sub) {
if($sub instanceof AST\PathExpression) {
$str .= $sub->identificationVariable . '.' . $sub->field;
if($expression instanceof AST\Functions\FunctionNode) {
$str = $expression->name . '(' . $str . ')';
} elseif($expression instanceof AST\AggregateExpression) {
$str = $expression->functionName . '(' . $str . ')';
}
//when we got another array, we will call the method recursive and add
//brackets for readability.
} elseif(is_array($sub)) {
$str .= '(' . $this->_getNameForExpression($sub) . ')';
//call the method recursive to get all names.
} elseif(is_object($sub)) {
$str .= $this->_getNameForExpression($sub);
//key is numeric and value is a string, we probably got an
//arithmetic identifier (like "-" or "/")
} elseif(is_numeric($key) && is_string($sub)) {
$str .= ' ' . $sub . ' ';
//we got a string value for example in an arithmetic expression
//(a.value - 1) the "1" here is the value we append to the string here
} elseif($key == 'value') {
$str .= $sub;
}
}
return $str;
}
/**
* Build the form based on a Model or query.
*
* @return array
*/
public function buildForm($inputsType = array())
{
$qb = $this->getQueryBuilder();
//create the form based on the main table
$mainTable = $this->getMainTable();
$em = $this->getEntityManager();
$metadata = $em->getClassMetadata($mainTable['table']);
return $this->buildFormElements($metadata->fieldMappings, $metadata, $inputsType);
}
/**
* Will build out an array of form elements,
* based on the column type and return the array
* to be used when loading the Bvb_Grid_Form
*
* @param array $cols
* @param array $info
* @return array
*/
public function buildFormElements(array $cols, $info = array(), $inputsType = array())
{
$form = array();
$em = $this->getEntityManager();
foreach($cols as $column => $detail) {
if(isset($detail['id']) && $detail['id']) {
continue;
}
$label = ucwords(str_replace('_', ' ', $column));
switch($detail['type']) {
case Type::STRING:
$length = (is_null($detail['length'])) ? 255 : $detail['length'];
$return[$column] = array('type' => 'smallText',
'length' => $length,
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::DATE:
$return[$column] = array('type' => 'date',
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::TIME:
$return[$column] = array('type' => 'time',
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::DATETIME:
case Type::DATETIMETZ:
$return[$column] = array('type' => 'datetime',
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::TEXT:
$return[$column] = array('type' => 'longtext',
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::INTEGER:
case Type::BIGINT:
case Type::SMALLINT:
$return[$column] = array('type' => 'number',
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::FLOAT:
case Type::DECIMAL:
$return[$column] = array('type' => 'decimal',
'label' => $label,
'required' => !$detail['nullable'],
'default' => "");
break;
case Type::BOOLEAN:
$return[$column] = array('type' => 'select',
'label' => $label,
'required' => $detail['nullable'],
'default' => array(true => 'Yes', false => 'No'));
default:
break;
}
}
if(count($info->associationMappings > 0)) {
foreach($info->associationMappings as $column => $detail) {
//many to many relations are not supported
if($detail['type'] == ClassMetadata::MANY_TO_MANY || $detail['isOwningSide'] == false) {
continue;
}
$label = ucwords(str_replace('_', ' ', $column));
$targetEntity = $detail['targetEntity'];
$dummy = new $targetEntity();
//check if the entity class got a __toString method
//if the class got one, we use this for the value in the select field
//otherwise use fetch an array and search for a display column.
if(method_exists($dummy, '__toString')) {
$hydrate = Query::HYDRATE_OBJECT;
} else {
$hydrate = Query::HYDRATE_ARRAY;
}
$metadata = $em->getClassMetadata($targetEntity);
$assoc = $em->getRepository($targetEntity)
->createQueryBuilder('c')
->getQuery()
->getResult($hydrate);
$primaryColumn = $metadata->identifier[0];
$displayField = null;
//seach for a field with the type string and use this value of the field for the select
foreach($metadata->fieldMappings as $fieldMapping) {
if($fieldMapping['type'] == Type::STRING) {
$displayField = $fieldMapping['fieldName'];
}
}
$final['values'][$column] = array();
//if no display field was found, use the primary column
$displayField = (is_null($displayField)) ? $primaryColumn : $displayField;
$isNullable = $detail['joinColumns'][0]['nullable'];
if($isNullable) {
$final['values'][$column][""] = "-- Empty --";
}
foreach($assoc as $field) {
if(is_object($field)) {
$method = 'get' . ucfirst($primaryColumn);
if(!method_exists($field, $method)) {
throw new Bvb_Grid_Source_Doctrine2_Exception('No getter method for the primary field found (used name: ' . $method . ').');
}
$final['values'][$column][$field->$method()] = $field->__toString();
} else {
$final['values'][$column][$field[$primaryColumn]] = $field[$displayField];
}
}
$return[$column] = array('type' => 'select',
'label' => $label,
'default' => $final['values'][$column]);
}
}
$form = $this->buildFormElementsFromArray($return);
foreach($inputsType as $field => $type) {
$form['elements'][$field][0] = strtolower($type);
}
return $form;
}
/**
* Build the query limit clause
*
* @param $start
* @param $offset
* @return Mp_Grid_Source_Doctrine2
*/
public function buildQueryLimit($start, $offset)
{
if($start == 0 && $offset == 0) {
$this->resetLimit();
} else {
$this->getQueryBuilder()->setMaxResults($start)->setFirstResult($offset);
}
return $this;
}
/**
* Build the order part from the query.
*
* The first arg is the field to be ordered and the $order
* arg is the correspondent order (ASC|DESC)
*
* If the $reset is set to true, all previous order will be removed.
*
* @param string $field
* @param string $order
* @param bool $reset
* @return Mp_Grid_Source_Doctrine2
*/
public function buildQueryOrder($field, $order, $reset = false)
{
//fetch the fieldname from the created fields array
$fieldName = $this->_getFieldName($field);
$qb = $this->getQueryBuilder();
if($reset) {
$qb->resetDQLPart('orderBy');
}
$qb->addOrderBy($fieldName, $order);
return $this;
}
/**
* Delete a record from a table
*
*
* Ex: array('user_id'=>'1','id_site'=>'12');
* Raw SQL: * WHERE user_id='1' AND id_site='12'
*
* @param string $table
* @param array $condition
* @return integer Of Affected rows
*/
public function delete($table, array $condition)
{
$qbDelete = new QueryBuilder($this->getEntityManager());
$alias = $this->_getNewAlias();
//create a delete query for the given entity
$qbDelete->delete($table, $alias);
$first = true;
foreach($condition as $column => $value) {
//remove alias and prepend own one
$column = $this->_removeAlias($column);
$column = $alias . '.' . $column;
$this->_addCondition($qbDelete, $column, '=', $value);
}
$return = $qbDelete->getQuery()->execute();
return $return;
}
/**
* Runs the query and returns the result as a associative array
*
* @return array
*/
public function execute()
{
$qb = $this->getQueryBuilder();
$qb = clone $qb;
try {
$result = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} catch(Query\QueryException $e) {
Zend_Debug::dump($e);
}
$result = $this->_cleanQueryResults($result);
return $result;
}
/**
* Get a record detail based the current query
*
* <code>
* $where = array(
* array('columnName' => 'searchValue')
* )
* </code>
*
* @param array $where
* @return array
*/
public function fetchDetail(array $where)
{
$qb = $this->getQueryBuilder();
/**
* Remove these since we are trying to retrieve
* a specific row
*/
$qb->setFirstResult(null)->setMaxResults(null);
$this->_createWhereConditions($qb, $where);
return $this->execute();
}
/**
* This method is not implemented in the interface (yet), nevertheless it
* has to be implemented so crud-operations can be used.
*
* The only thing that is done here, is the creation of a new field 'NULLABLE',
* which defines wether or not the field is nullable.
*
* @param array|string $table Table Name
* @return array
*/
public function getDescribeTable($class)
{
$metadata = $this->getEntityManager()->getClassMetadata($class);
$fieldMappings = $metadata->fieldMappings;
$return = array();
foreach($fieldMappings as $key => $fields) {
$return[$key] = $fields;
$return[$key]['NULLABLE'] = ($fields['nullable']) ? 1 : 0;
}
$associationMappings = $metadata->associationMappings;
foreach($associationMappings as $key => $detail) {
if($detail['type'] == ClassMetadata::MANY_TO_MANY || $detail['isOwningSide'] == false) {
continue;
}
$return[$key]['fieldName'] = $key;
$return[$key]['NULLABLE'] = ($detail['joinColumns'][0]['nullable']) ? 1 : 0;
}
return $return;
}
/**
* Should preform a query based on the provided by the user
* select the two fields and return an array $field=>$value
* as result
*
* ex: SELECT $field, $value FROM *
* array('1'=>'Something','2'=>'Number','3'=>'history')....;
*
* @param string $field
* @param string $value
* @return array
*/
public function getDistinctValuesForFilters($field, $fieldValue, $order = 'name ASC')
{
$return = array();
$newQuery = clone $this->getQueryBuilder();
$newQuery->resetDQLPart('select')
->resetDQLPart('orderBy')
->setMaxResults(null)
->setFirstResult(null);
$newQuery->select("DISTINCT(" . $field . ") AS field, " . $fieldValue . " AS value")
->orderBy($fieldValue, "ASC");
$result = $newQuery->getQuery()->getResult(Query::HYDRATE_ARRAY);
foreach($result as $value) {
$return[$value['field']] = $value['value'];
}
return $return;
}
/**
* Return te field type
* char, varchar, int
*
* Note: If the field is enum or set,
* the value returned must be set or enum,
* and not the full definition
*
* @param string $field
* @return string|null
*/
public function getFieldType($field)
{
//check if the type was already fetched
if(!isset($this->fields[$field]['type'])) {
//get the field with the alias
$fieldName = $this->_getFieldName($field);
//remove an existing alias
$field = $this->_removeAlias($field);
$tableModel = $this->_getModelFromColumn($fieldName);
//fetch data type from metadata
try {
if($tableModel !== null) {
$metadata = $this->getEntityManager()->getClassMetadata($tableModel);
}
} catch(Exception $e) {
$this->fields[$field]['type'] = Type::STRING;
}
if(isset($metadata->fieldMappings[$field]['type'])) {
$this->fields[$field]['type'] = $metadata->fieldMappings[$field]['type'];
} else {
$this->fields[$field]['type'] = Type::STRING;
}
}
return $this->fields[$field]['type'];
}
/**
* Return possible filters values based on field definition
* This is mostly used for enum fields where the possibile
* values are extracted
*
* Ex: enum('Yes','No','Empty');
*
* should return
*
* array('Yes'=>'Yes','No'=>'No','Empty'=>'Empty');
*
* @param $field
* @return string
*/
public function getFilterValuesBasedOnFieldDefinition($field)
{
//since there is no enum support in doctrine 2, we only return 'text'
return 'text';
}
/**
* Find identifier columns.
*
* @return array Primary Keys for specified table
*/
public function getIdentifierColumns($table = null)
{
$return = array();
if(is_null($table)) {
$mainTable = $this->getMainTable();
$table = $mainTable['table'];
}
$metadata = $this->getEntityManager()->getClassMetadata($table);
$identifier = $metadata->identifier;
$fromPart = $this->getQueryBuilder()->getDQLPart('from');
$alias = $fromPart[0]->getAlias();
foreach($identifier as $id) {
$return[] = $alias . '.' . $id;
}
return $return;
}
/**
* Returns the "main" table
* the one after SELECT * FROM {MAIN_TABLE}
*
* @return array
*/
public function getMainTable()
{
$fromPart = $this->getQueryBuilder()->getDQLPart('from');
return array('table' => $fromPart[0]->getFrom());
}
/**
* Returns tables primary keys separeted by commas ","
* This is necessary for mass actions
* @param $table
*/
public function getMassActionsIds($table, $fields, $separator = '-')
{
$qb = new QueryBuilder($this->getEntityManager());
if(count($fields) == 0) {
$metadata = $this->getEntityManager()->getClassMetadata($table);
$pks = $metadata->identifier;
} else {
$pks = $fields;
}
//the alias used in this query
$alias = 'm';
$qb->from($table, 'm');
foreach($pks as $key => $pk) {
//remove any existing alias and add the one used in this query
$pk = $this->_removeAlias($pk);
$pks[$key] = $alias . '.' . $pk;
}
$qb->select(implode(',', $pks));
try {
$result = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} catch(Query\QueryException $e) {
return array();
}
$return = array();
foreach($result as $value) {
$return[] = implode($separator, $value);
}
return implode(',', $return);
}
/**
* Returns a specified record
*
* @param string $table Table Name
* @param array $condition Conditions to build query
*
* @return false|array
*/
public function getRecord($table, array $condition)
{
$em = $this->getEntityManager();
$qb = new QueryBuilder($em);
$alias = $this->_getNewAlias();
$newCondition = array();
//remove alias and set the one used for this query
foreach($condition as $fieldName => $value) {
$field = $this->_removeAlias($fieldName);
$field = $alias . '.' . $field;
$newCondition[$field] = $value;
}
$metadata = $em->getClassMetadata($table);
$select = $alias;
//create a query where all fields are contained, even the associations
foreach($metadata->associationMappings as $column => $detail) {
//skip relations where the type is many to many or this side is not the
//owning side
if($detail['type'] == ClassMetadata::MANY_TO_MANY || $detail['isOwningSide'] == false) {
continue;
}
$joinAlias = $this->_getNewAlias();
$refColumn = $detail['joinColumns'][0]['referencedColumnName'];
//join the table
$qb->leftJoin($alias . '.' . $detail['fieldName'], $joinAlias);
//append primary key from the joined table to the select
//and use the defined column "AS" the name
$select .= ', ' . $joinAlias . '.' . $refColumn . ' AS ' . $column;
}
$qb->from($table, $alias)
->select($select);
$this->_createWhereConditions($qb, $newCondition);
try {
$result = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
$result = $this->_cleanQueryResults($result);
} catch(Query\QueryException $e) {
$result[0] = array();
Zend_Debug::dump($e);
}
return $result[0];
}
/**
* Returns the select object
*
* @return QueryBuilder
*/
public function getSelectObject()
{
return $this->getQueryBuilder();
}
/**
* Returns the selected order
* that was defined by the user in the query entered
* and not the one generated by the system
*
* If empty an empty array must be returned.
*
* Else the array must be like this:
*
* <code>
* $return = array(
* 0 => field
* 1 => (ASC|DESC)
* );
* </code>
*
* @return array
*/
public function getSelectOrder()
{
$qb = $this->getQueryBuilder();
$orderBy = $qb->getDqlPart('orderBy');
if(empty($orderBy)) {
return array();
}
$AST = $qb->getQuery()->getAST();
$orderByClause = $AST->orderByClause;
$orderByItem = $orderByClause->orderByItems[0];
if(is_string($orderByItem->expression)) {
//the expressin is a string, use the value directly
$return[0] = $orderByItem->expression;
} else {
//use alias and field name
$return[0] = $orderByItem->expression->identificationVariable . '.' . $orderByItem->expression->field;
}
$return[1] = $orderByItem->type;
return $return;
}
/**
* Return the database driver name.
*
* Ex: mysql, pgsql, array, xml
*
* @return string
*/
public function getSourceName()
{
$driver = $this->getEntityManager()->getConnection()->getDriver();
$adapter = str_ireplace('pdo_', '', $driver->getName());
return strtolower($adapter);
}
/**
* Perform a sqlexp
*
* $value = array ('functions' => array ('AVG'), 'value' => 'Population' );
*
* Should be converted to
* SELECT AVG(Population) FROM *
*
* $value = array ('functions' => array ('SUM','AVG'), 'value' => 'Population' );
*
* Should be converted to
* SELECT SUM(AVG(Population)) FROM *
*
* @param array $value
*/
public function getSqlExp(array $value, $where = array())
{
$return = array();
$qb = $this->getQueryBuilder();
$qb = clone $qb;
foreach(array_reverse($value['functions']) as $key => $func) {
if($key == 0) {
$exp = $func . '(' . $value['value'] . ')';
} else {
$exp = $func . '(' . $exp . ')';
}
}
$qb->resetDQLPart('select')
->resetDQLPart('orderBy')
->setMaxResults(null)
->setFirstResult(null)
->select($exp . ' AS TOTAL');
return $qb->getQuery()->getScalarResult();
}
public function getTableList()
{
throw new Bvb_Grid_Source_Doctrine2_Exception('Not yet implemented.');
}
/**
* Return the total number of records.
*
* @return integer
*/
public function getTotalRecords()
{
$qb = $this->getQueryBuilder();
$qb = clone $qb;
$qb->setFirstResult(null)
->setMaxResults(null)
->resetDQLPart('orderBy');
$AST = $qb->getQuery()->getAST();
$hasExpr = false;
foreach($AST->selectClause->selectExpressions as $selectExpressions) {
if($selectExpressions->expression instanceof Query\AST\AggregateExpression) {
$hasExpr = true;
}
}
if($hasExpr) {
$result = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
return count($result);
}
$qb->resetDQLPart('select');
$fromPart = $qb->getDQLPart('from');
$qb->select('COUNT(DISTINCT ' . $fromPart[0]->getAlias() . ')');
return $qb->getQuery()->getSingleScalarResult();
}
/**
* @todo Implement
* @see library/Bvb/Grid/Source/Bvb_Grid_Source_SourceInterface::getValuesForFiltersFromTable()
*/
public function getValuesForFiltersFromTable($table, $field, $fieldValue, $order = 'name ASC')
{
throw new Bvb_Grid_Source_Doctrine2_Exception('Not yet Implemented.');
}
/**
* Simple method for the Grid to determine if this
* Source can handle CRUD
*
* @return boolean
*/
public function hasCrud()
{
return true;
}
/**
* Insert an array of key=>values in the specified table
*
* @param string $table
* @param array $post
* @return boolean
*/
public function insert($table, array $post)
{
$entity = new $table();
$post = $this->_setReferences($table, $post);
$this->_setEntityValues($entity, $post);
$em = $this->getEntityManager();
$em->persist($entity);
$em->flush();
}
/**
*
* Quotes a string
*
* @param string $value Field Value
*/
public function quoteValue($value)
{
return $this->getEntityManager()->getConnection()->quote($value);
}
/**
* Removes any limit in query
*
* @return Bvb_Grid_Source_Doctrine
*/
public function resetLimit()
{
$qb = $this->getQueryBuilder();
$qb->setMaxResults(null)
->setFirstResult(null);
return $this;
}
/**
* Removes any order in query
*
* @return Bvb_Grid_Source_Doctrine
*/
public function resetOrder()
{
$this->getQueryBuilder()->resetDQLPart('orderBy');
return $this;
}
/**
* Cache handler.
*
* @param Zend_Cache
*/
public function setCache($cache)
{
}
/**
* Update values in a table using the $condition clause
*
* The condition clause is a $field=>$value array
*
* Ex: array('user_id'=>'1','id_site'=>'12');
*
* Raw SQL: * WHERE user_id='1' AND id_site='12'
*
* @param string $table
* @param array $post
* @param array $condition
*/
public function update($table, array $post, array $condition)
{
$em = $this->getEntityManager();
$newCondition = array();
foreach($condition as $fieldName => $value) {
$field = $this->_removeAlias($fieldName);
$newCondition[$field] = $value;
}
$post = $this->_setReferences($table, $post);
$entity = $em->getRepository($table)->findOneBy($newCondition);
$this->_setEntityValues($entity, $post);
$em->persist($entity);
$em->flush();
return $this;
}
/**
* Creates for association fields a reference instead of the id itself.
*
* @param string $table
* @param array $post
*/
private function _setReferences($table, array $post)
{
$em = $this->getEntityManager();
$metadata = $em->getClassMetadata($table);
foreach($post as $fieldName => $value) {
if(isset($metadata->associationMappings[$fieldName])) {
if(empty($value)) {
$post[$fieldName] = NULL;
} else {
$targetEntity = $metadata->associationMappings[$fieldName]['targetEntity'];
$post[$fieldName] = $em->getRepository($targetEntity)->find($value);
}
}
}
return $post;
}
public function getAutoCompleteForFilter($term, $field, $specialKey = '', $output = 'json')
{
$qb = $this->getQueryBuilder();
$qb = clone $qb;
$fieldName = $this->_getFieldName($field);
//clear where part and bound parameters
$qb->resetDQLPart('where');
$qb->setParameters(array());
foreach($this->whereConditions as $key => $detail) {
if($key != $fieldName) {
$this->_addCondition($qb, $key, $detail['op'], $detail['filter']);
}
}
//add condition for the given field
$op = ($specialKey != '') ? $specialKey : 'like';
$this->_addCondition($qb, $fieldName, $op, $term);
$qb->select('DISTINCT ' . $fieldName);
$result = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
$result = $this->_cleanQueryResults($result);
$json = array();
foreach($result as $row) {
$json[] = $specialKey . $row[$field];
}
echo Zend_Json::encode($json);
die();
}
/**
* Defines total records
*
* @param int $total Total records found
*/
public function setTotalRecords($total)
{
$this->_totalRecords = (int) $total;
}
/**
* Clean a query result.
*
* Doctrine2 uses a DateTime object for date/datetime types.
* Hence the grid can not use them correctly, this types will be convertet to a string.
*
* @param array $result
* @return array
*/
private function _cleanQueryResults(array $result)
{
$newResult = array();
foreach($result as $key => $values) {
foreach($values as $name => $value) {
if(is_array($value)) {
foreach($value as $k => $v) {
$newResult[$key][$k] = $this->_convertResult($k, $v);
}
} else {
$newResult[$key][$name] = $this->_convertResult($name, $value);
}
}
}
return $newResult;
}
private function _getNewAlias()
{
$alias = chr($this->alias);
$this->alias++;
return $alias;
}
private function _getNewParameterNumber()
{
return $this->paramterNumber++;
}
/**
* Convert datetime object to readable strings and booleans to 0/1.
*
* @param string $field The fieldname.
* @param mixed $value The value to be converted.
* @return mixed The converted value.
*/
private function _convertResult($field, $value)
{
if($value instanceof DateTime) {
$type = $this->getFieldType($field);
//format the field depending on the field type
if($type == Type::DATE) {
return $value->format('Y-m-d');
} elseif($type == Type::DATETIME) {
return $value->format('Y-m-d H:i');
}
} elseif(is_bool($value)) {
//convert boolean types to integer, so the default value is correctly set
//in the form element
return ($value) ? 1 : 0;
}
return $value;
}
/**
* Sets all values in an entity.
*
* @param object $entity
* @param array $values
*/
private function _setEntityValues($entity, array $values)
{
foreach($values as $key => $value) {
$method = 'set' . ucfirst($key);
$type = $this->getFieldType($key);
//if the column is from type date or datetime, create a DateTime object
if($type == Type::DATE || $type == Type::DATETIME) {
$value = new DateTime($value);
}
if(method_exists($entity, $method)) {
$entity->$method($value);
}
}
}
/**
* Find the table for which a column belongs.
*
* @param string $column
* @return string Name of the table used
*/
private function _getModelFromColumn($column)
{
if(!is_string($column)) {
$type = gettype($column);
require_once 'Bvb/Grid/Source/Doctrine/Exception.php';
throw new Bvb_Grid_Source_Doctrine2_Exception('The $column param needs to be a string, ' . $type . ' provided');
}
if(strpos($column, '.') === false) {
return null;
}
list($alias, $field) = explode('.', $column);
return $this->_getModelFromAlias($alias);
}
/**
* Finds a model for which an alias belongs.
*
* @param string $alias
* @return string The name of the entity.
*/
private function _getModelFromAlias($alias)
{
$qb = $this->getQueryBuilder();
$fromParts = $qb->getDQLPart('from');
//first try to get the model from the from part
foreach($fromParts as $fromPart) {
if($fromPart->getAlias() == $alias) {
return $fromPart->getFrom();
}
}
//when the from part doesnt have it, we first find the join field defined
//by the alias
$AST = $qb->getQuery()->getAST();
$field = null;
foreach($AST->fromClause->identificationVariableDeclarations[0]->joinVariableDeclarations as $joinVariable) {
if($alias == $joinVariable->join->aliasIdentificationVariable) {
$field = $joinVariable->join->joinAssociationPathExpression->associationField;
break;
}
}
if(is_null($field)) {
throw new Bvb_Grid_Source_Doctrine2_Exception("No field found.");
}
//iterate over the fromparts, get the metadata from it and
//iterate then over the association mappings to find the specific
//model for the alias
foreach($fromParts as $fromPart) {
$metadata = $this->getEntityManager()->getClassMetadata($fromPart->getFrom());
foreach($metadata->associationMappings as $mapping) {
if($mapping['fieldName'] == $field) {
return $mapping['targetEntity'];
}
}
}
throw new Bvb_Grid_Source_Doctrine2_Exception("No model found.");
}
/**
* Creates the where conditions for a query builder.
*
* @param QueryBuilder $qb
* @param array $where
* @return QueryBuilder
*/
private function _createWhereConditions(QueryBuilder $qb, array $where)
{
foreach($where as $column => $value) {
$this->_addCondition($qb, $column, '=', $value);
}
return $qb;
}
/**
* Removes the alias from the given field.
*
* @param string $field
* @return string field withou alias
*/
private function _removeAlias($field)
{
if(strpos($field, '.') !== false) {
list($alias, $column) = explode('.', $field);
} else {
$column = $field;
}
return $column;
}
/**
* Retrieves the complete fieldname of a column (with the alias).
*
* @param string $field
* @return string Complete fieldname
*/
private function _getFieldName($field)
{
if(strpos($field, '.') === false) {
if(!isset($this->fields[$field]['field'])) {
$ids = $this->getIdentifierColumns();
list($alias, $field) = explode('.', $ids[0]);
//no matching field was found in the fields array
//so we append the default alias to the field
return $alias .'.'. $field;
}
return $this->fields[$field]['field'];
}
return $field;
}
public function beginTransaction()
{
return false;
}
public function commit()
{
return false;
}
public function rollBack()
{
return false;
}
public function getConnectionId()
{
return 0;
}
}
|