diff --git a/Datatables/Datatable.php b/Datatables/Datatable.php index 8b746f0..4c99b4f 100755 --- a/Datatables/Datatable.php +++ b/Datatables/Datatable.php @@ -62,11 +62,33 @@ class Datatable */ const RESULT_RESPONSE = 'Response'; + /** + * Column property: sTitle + */ + const COLUMN_TITLE = 'sTitle'; + + /** + * Column property: searchable + */ + const COLUMN_SEARCHABLE = 'bSearchable'; + + /** + * Column property: sortable + */ + const COLUMN_SORTABLE = 'bSortable'; + + /** + * Custom variable: sDtRowIdPrefix + */ + const CUSTOM_VAR_ROW_ID_PREFIX = 'sDtRowIdPrefix'; + /** * @var array Holds callbacks to be used */ protected $callbacks = array( 'WhereBuilder' => array(), + 'WhereCollection' => array(), + 'columnFilter' => array() ); /** @@ -84,6 +106,11 @@ class Datatable */ protected $useDtRowId = false; + /** + * @var string If $useDtRowId is set to true then an id will be appended to each row, you can also specify a string to be concatenated in the beginning of each row id + */ + protected $dtRowIdPrefix; + /** * @var string Whether or not to add DT_RowClass to each record if it is set */ @@ -134,6 +161,16 @@ class Datatable */ protected $parameters; + /** + * @var array A map with columns server-side defined + */ + protected $aoColumns; + + /** + * @var array A map with custom vars server-side defined + */ + protected $aoCustomVars; + /** * @var array Information relating to the specific columns requested */ @@ -194,25 +231,50 @@ class Datatable */ protected $datatable; - public function __construct(array $request, EntityRepository $repository, ClassMetadata $metadata, EntityManager $em, $serializer) + /** + * @var boolean A flag to control where aoColumns.mDataProp is defined, if used on server side then you need to use addColumn method + */ + protected $serverSideControl; + + /** + * @var array A map between column key name and the association map dql fullName + */ + protected $columnsDqlPartName; + + public function __construct(array $request, EntityRepository $repository, ClassMetadata $metadata, EntityManager $em, $serializer, $serverSideControl) { $this->em = $em; $this->request = $request; $this->repository = $repository; $this->metadata = $metadata; $this->serializer = $serializer; + $this->serverSideControl = $serverSideControl; $this->tableName = Container::camelize($metadata->getTableName()); $this->defaultJoinType = self::JOIN_INNER; $this->defaultResultType = self::RESULT_RESPONSE; - $this->setParameters(); + if ($this->serverSideControl === false) { + if (sizeof($this->request) == 0 || count(array_diff(array('iColumns', 'sEcho', 'sSearch', 'iDisplayStart', 'iDisplayLength'), array_keys($this->request)))) { + throw new \Exception('Unable to recognize a datatables.js valid request.'); + } + $this->setParameters(); + } $this->qb = $em->createQueryBuilder(); - $this->echo = $this->request['sEcho']; - $this->search = $this->request['sSearch']; - $this->offset = $this->request['iDisplayStart']; - $this->amount = $this->request['iDisplayLength']; $identifiers = $this->metadata->getIdentifierFieldNames(); $this->rootEntityIdentifier = array_shift($identifiers); + + // Default vars to inject into 'aoCustomVars' when using server side control + $this->aoCustomVars = array(); + + if (sizeof($this->request) > 0) { + $this->echo = $this->request['sEcho']; + $this->search = $this->request['sSearch']; + $this->offset = $this->request['iDisplayStart']; + $this->amount = $this->request['iDisplayLength']; + $this->dtRowIdPrefix = isset($this->request[self::CUSTOM_VAR_ROW_ID_PREFIX]) + ? $this->request[self::CUSTOM_VAR_ROW_ID_PREFIX] + : ''; + } } /** @@ -272,7 +334,8 @@ public function setParameters() $params = array(); $associations = array(); for ($i=0; $i < intval($this->request['iColumns']); $i++) { - $fields = explode('.', $this->request['mDataProp_' . $i]); + $key = $this->request['mDataProp_' . $i]; + $fields = explode('.', $key); $params[] = $this->request['mDataProp_' . $i]; $associations[] = array('containsCollections' => false); @@ -286,6 +349,110 @@ public function setParameters() } } + /** + * Add a new column to the Datatable object + * + * Parse and configure parameter/association using a per addColumn basis and also configures aoColumns to be used + * as a retrieved object by DataTables.js (it automatically includes mDataProp using the provided $key value) + * + * @param $key A dotted-notation property format key used by DQL to fetch your object. Use the property from the object that you provided to getDatatable() method + * @param array $rawOptions (optional) A map with raw keys used by datatables.js aoColumns property + * @param callback $filterCallback (optional) A filter callback to be applied to the current column after retrieved from QueryBuilder; + */ + public function addColumn($key, $rawOptions = null, $filterCallback = null) + { + if (!$this->serverSideControl) { + throw new \Exception(sprintf("The \"%s\" method is not allowed to use if you are not using server-side control datatables.", __FUNCTION__)); + } + + if (is_null($rawOptions)) { + $rawOptions = array(); + } + + $rawOptions['mDataProp'] = $key; + $this->aoColumns[] = $rawOptions; + $this->parameters[] = $key; + + $fields = explode('.', $key); + $association = array('containsCollections' => false); + + if (count($fields) > 1) { + $this->setRelatedEntityColumnInfo($association, $fields); + } else { + $this->setSingleFieldColumnInfo($association, $fields[0]); + } + + $this->associations[] = $association; + if (!is_null($filterCallback)) { + $this->addColumnFilter($key, $filterCallback); + } + + return $this; + } + + /** + * Adds a function to filter the value returned by key + * + * @param string $key Your key name + * @param callback $filterCallback The function used to filter the result value of $key + * @throws \Exception If the filterCallback is not a callable function + */ + public function addColumnFilter($key, $filterCallback) + { + if (!is_callable($filterCallback)) { + throw new \Exception(sprintf("The second argument of \"%s\" method must be a callable function", __FUNCTION__)); + } + if (!isset($this->callbacks['columnFilter'][$key])) { + $this->callbacks['columnFilter'][$key] = array(); + } + $this->callbacks['columnFilter'][$key][] = $filterCallback; + } + + /** + * Gets the DQL field name of a DataTables column + * + * @param string $key A key used as reference to a DataTables column + * @return string|null The DQL field name extracted from DataTables column key + */ + public function getColumnDQLPartName($key) + { + if (!isset($this->columnsDqlPartName[$key])) { + throw new \Exception(sprintf( + "A missing key ['%s'] was detected in your datatable object when \"%s()\" method was called.", + $key, + __FUNCTION__ + )); + } + + return $this->columnsDqlPartName[$key]; + } + + /** + * Automatically sets the DQL field name of a DataTables column based on its key + * + * You should use this method when you need to call getColumnDQLPartName method inside a filter callback for a entity + * field that does not belongs to your datatable.js instance, but somehow you need to use it to do some filtering or + * whatever. + * + * @param string $key A dotted notation key value of your entity field + * @return Datatable + */ + public function addColumnDQLPartName($key) + { + $fields = explode('.', $key); + $association = array('containsCollections' => false); + + if (count($fields) > 1) { + $this->setRelatedEntityColumnInfo($association, $fields); + } else { + $this->setSingleFieldColumnInfo($association, $fields[0]); + } + + $this->columnsDqlPartName[$key] = $association['fullName']; + + return $this; + } + /** * Parse a dotted-notation column format from the mData, and sets association * information @@ -344,6 +511,7 @@ protected function setRelatedEntityColumnInfo(array &$association, array $fields $association['fieldName'] = $lastField; $association['joinName'] = $joinName; $association['fullName'] = $this->getFullName($association); + $this->columnsDqlPartName[$mdataName] = $association['fullName']; } /** @@ -353,6 +521,7 @@ protected function setRelatedEntityColumnInfo(array &$association, array $fields * @param string The field name on the main entity */ protected function setSingleFieldColumnInfo(array &$association, $fieldName) { + $key = $fieldName; $fieldName = Container::camelize($fieldName); if (!$this->metadata->hasField(lcfirst($fieldName))) { @@ -365,6 +534,7 @@ protected function setSingleFieldColumnInfo(array &$association, $fieldName) { $association['fieldName'] = $fieldName; $association['entityName'] = $this->tableName; $association['fullName'] = $this->tableName . '.' . lcfirst($fieldName); + $this->columnsDqlPartName[$key] = $association['fullName']; } /** @@ -511,6 +681,19 @@ public function setWhere(QueryBuilder $qb) $callback($qb); } } + + if (!empty($this->callbacks['WhereCollection'])) { + foreach ($this->callbacks['WhereCollection'] as $callback) { + $whereCollection = $callback($qb->expr()); + if (!is_array($whereCollection)) { + throw new \Exception(sprintf("The function %s must return an array", $callback)); + } + + if (sizeof($whereCollection) == 0) continue; + + $qb->andWhere($qb->expr()->andX()->addMultiple($whereCollection)); + } + } } /** @@ -606,20 +789,25 @@ public function executeSearch() $output = array("aaData" => array()); $query = $this->qb->getQuery()->setHydrationMode(Query::HYDRATE_ARRAY); + $items = $this->useDoctrinePaginator ? new Paginator($query, $this->doesQueryContainCollections()) : $query->execute(); foreach ($items as $item) { // Go through each requested column, transforming the array as needed for DataTables for ($i = 0 ; $i < count($this->parameters); $i++) { + $parameterKey = $this->parameters[$i]; if ($this->useDtRowClass && !is_null($this->dtRowClass)) { $item['DT_RowClass'] = $this->dtRowClass; } if ($this->useDtRowId) { - $item['DT_RowId'] = $item[$this->rootEntityIdentifier]; + $item['DT_RowId'] = $this->dtRowIdPrefix . $item[$this->rootEntityIdentifier]; } // Results are already correctly formatted if this is the case... if (!$this->associations[$i]['containsCollections']) { + $item[$parameterKey] = isset($item[$parameterKey]) ? $item[$parameterKey] : null; // Inject missing parameters + $this->applyColumnFiltering($parameterKey, $item[$parameterKey]); // Apply column filtering if needed + continue; } @@ -636,9 +824,13 @@ public function executeSearch() $children = array_merge_recursive($children, $childItem); } $rowRef = $children; + } else { // Only leaf nodes... + $rowRef[$field] = isset($rowRef[$field]) ? $rowRef[$field] : null; // Inject missing parameters + $this->applyColumnFiltering($parameterKey, $rowRef); } } } + $output['aaData'][] = $item; } @@ -648,11 +840,37 @@ public function executeSearch() "iTotalDisplayRecords" => $this->getCountFilteredResults() ); + if ($this->serverSideControl) { + $outputHeader['aoColumns'] = $this->aoColumns; + $outputHeader['aoCustomVars'] = $this->getAoCustomVars(); + } + $this->datatable = array_merge($outputHeader, $output); return $this; } + /** + * Apply a columnFilter to the column value $value identified by $key. + * + * @param string $key Column key + * @param mixed $value A value of any type passed by reference + * @return bool It return false if no filtering was applied to the column, or true if the filter was applied + */ + private function applyColumnFiltering($key, &$value) + { + if (!isset($this->callbacks['columnFilter'][$key])) { + return false; + } + + $columnFilterCallback = $this->callbacks['columnFilter'][$key]; + foreach ($columnFilterCallback as $callback) { + $value = $callback($value); + } + + return true; + } + /** * @return boolean Whether any mData contains an association that is a collection */ @@ -738,12 +956,27 @@ public function getCountAllResults() $qb = $this->repository->createQueryBuilder($this->tableName) ->select('count(' . $this->tableName . '.' . $this->rootEntityIdentifier . ')'); + $this->setAssociations($qb); + if (!empty($this->callbacks['WhereBuilder']) && $this->hideFilteredCount) { foreach ($this->callbacks['WhereBuilder'] as $callback) { $callback($qb); } } + if (!empty($this->callbacks['WhereCollection']) && $this->hideFilteredCount) { + foreach ($this->callbacks['WhereCollection'] as $callback) { + $whereCollection = $callback($qb->expr()); + if (!is_array($whereCollection)) { + throw new \Exception(sprintf("The function %s must return an array", $callback)); + } + + if (sizeof($whereCollection) == 0) continue; + + $qb->andWhere($qb->expr()->andX()->addMultiple($whereCollection)); + } + } + return (int) $qb->getQuery()->getSingleScalarResult(); } @@ -771,6 +1004,18 @@ public function addWhereBuilderCallback($callback) { return $this; } + /** + * @param object A callback function to be used at the end of 'setWhere' + */ + public function addWhereCollectionCallback($callback) { + if (!is_callable($callback)) { + throw new \Exception("The callback argument must be callable."); + } + $this->callbacks['WhereCollection'][] = $callback; + + return $this; + } + public function getOffset() { return $this->offset; @@ -795,4 +1040,49 @@ public function getQueryBuilder() { return $this->qb; } + + /** + * Add a custom variable key value pair to aoCustomVars custom object + * @param $key The key name + * @param $value The value for key + */ + public function addCustomVar($key, $value) + { + $this->aoCustomVars[$key] = $value; + } + + /** + * Gets custom variables + * @param bool $formatted If true it will convert the key value map into a 'name' => $key, 'value' => $value object array as the standard pattern of datatables.js (optional. Default: false) + * @return array A key value map of custom vars + */ + public function getAoCustomVars($formatted = false) + { + if (!$formatted) { + return $this->aoCustomVars; + } + + $oArr = array(); + foreach ($this->getAoCustomVars() as $key => $value) { + $oArr[] = array('name' => $key, 'value' => $value); + } + + return $oArr; + } + + /** + * Sets a prefix for the DT_RowId + * (NOTE: this will be returned as a custom variable, you need to treat in your front-end app since datatables.js + * doesn't support it by default, you can use a callback to check server params, if 'aoCustomVars' exists and + * a 'sDtRowIdPrefix' is defined then just add the key/value pair to the 'fnServerParams' callback, this will force + * datatables to send the same variable back to the server again, then the server will process and parse that + * message to append properly the prefix to each row id) + * + * @param string $dtRowIdPrefix + */ + public function setDtRowIdPrefix($dtRowIdPrefix) + { + $this->dtRowIdPrefix = $dtRowIdPrefix; // This doesn't make difference since its the client side who really decides about the prefix + $this->addCustomVar(self::CUSTOM_VAR_ROW_ID_PREFIX, $dtRowIdPrefix); // This will make the things happen, it will add a custom var to be treated in the front-end side + } } diff --git a/Datatables/DatatableManager.php b/Datatables/DatatableManager.php index b7edf4f..128287f 100755 --- a/Datatables/DatatableManager.php +++ b/Datatables/DatatableManager.php @@ -1,68 +1,70 @@ -doctrine = $doctrine; - $this->container = $container; - $this->useDoctrinePaginator = $useDoctrinePaginator; - } - - /** - * Given an entity class name or possible alias, convert it to the full class name - * - * @param string The entity class name or alias - * @return string The entity class name - */ - protected function getClassName($className) { - if (strpos($className, ':') !== false) { - list($namespaceAlias, $simpleClassName) = explode(':', $className); - $className = $this->doctrine->getManager()->getConfiguration() - ->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; - } - return $className; - } - - /** - * @param string An entity class name or alias - * @return object Get a DataTable instance for the given entity - */ - public function getDatatable($class) - { - $class = $this->getClassName($class); - - $metadata = $this->doctrine->getManager()->getClassMetadata($class); - $repository = $this->doctrine->getRepository($class); - - $datatable = new Datatable( - $this->container->get('request')->query->all(), - $this->doctrine->getRepository($class), - $this->doctrine->getManager()->getClassMetadata($class), - $this->doctrine->getManager(), - $this->container->get('lankit_datatables.serializer') - ); - return $datatable->useDoctrinePaginator($this->useDoctrinePaginator); - } -} - +doctrine = $doctrine; + $this->container = $container; + $this->useDoctrinePaginator = $useDoctrinePaginator; + } + + /** + * Given an entity class name or possible alias, convert it to the full class name + * + * @param string The entity class name or alias + * @return string The entity class name + */ + protected function getClassName($className) { + if (strpos($className, ':') !== false) { + list($namespaceAlias, $simpleClassName) = explode(':', $className); + $className = $this->doctrine->getManager()->getConfiguration() + ->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; + } + return $className; + } + + /** + * @param string $class An entity class name or alias + * @param boolean $serverSideControl This is a control flag to choose if the access to DataTables information will be controlled on server side or not (default: false) + * @return object Get a DataTable instance for the given entity + */ + public function getDatatable($class, $serverSideControl = false) + { + $class = $this->getClassName($class); + + $metadata = $this->doctrine->getManager()->getClassMetadata($class); + $repository = $this->doctrine->getRepository($class); + + $datatable = new Datatable( + $this->container->get('request')->query->all(), + $this->doctrine->getRepository($class), + $this->doctrine->getManager()->getClassMetadata($class), + $this->doctrine->getManager(), + $this->container->get('lankit_datatables.serializer'), + $serverSideControl + ); + return $datatable->useDoctrinePaginator($this->useDoctrinePaginator); + } +} + diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 1077e2a..0dee1e8 100755 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -5,16 +5,18 @@ Getting Started With LanKitDatatablesBundle * [Installation](#installation) * [Usage](#usage) * [Entity Associations and Join Types](#entity-associations-and-join-types) +* [Using Server Side Control](#server-side-control) * [Search Result Response Types](#search-result-response-types) * [Pre-Filtering Search Results](#pre-filtering-search-results) * [DateTime Formatting](#datetime-formatting) +* [Column Post-filtering](#column-post-filtering) * [DT_RowId and DT_RowClass](#dt_rowid-and-dt_rowclass) * [The Doctrine Paginator and MS SQL](#the-doctrine-paginator-and-ms-sql) This bundle provides an intuitive way to process DataTables.js requests by using mData. The mData from the DataTables request corresponds to fields and associations on a specific entity. You can access related entities off the -base entity by using dottted notation. +base entity by using dotted notation. For example, a mData structure to query an entity may look like the following: @@ -35,6 +37,24 @@ limitations with entity associations. If an association is a collection (ie. many associated records), then an array of values are returned for the final field in question. +This bundle supports two kind of approaches: the default one is totally controlled +by the front-end application, it means that it will parse any required information +that your datatables.js would require through the `aoColumns` array of `mData` +properties. The second one is called `serverSideControl`, using this approach you +can restrict the information available for a user. It will throw an error if the +user injects a malicious javascript code to require restricted information, like: + +``` js + "aoColumns": [ + { "mData": "email" }, + { "mData": "password" } + ] +``` + +Behind the scenes `aoColumns` still in control of the information exchange with +the bundle, but its the bundle who will provided datatable.js the information of +which columns are available. + ## Prerequisites This version of the bundle requires Symfony 2.1+. This bundle also needs the JMSSerializerBundle @@ -158,12 +178,19 @@ public function getDatatableAction() { $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer'); + /* + * This is automatically for any coloumn of your dataTable, but on this case customer.isActive is + * a filtering criteria to be used inside a callback, so you need to add it manually. If the + * filtered variable is a column then you don't need to do anything. + */ + $dataTable->addColumnDQLPartName('customer.isActive'); + // Add the $datatable variable, or other needed variables, to the callback scope $datatable->addWhereBuilderCallback(function($qb) use ($datatable) { $andExpr = $qb->expr()->andX(); - // The entity is always referred to using the CamelCase of its table name - $andExpr->add($qb->expr()->eq('Customer.isActive','1')); + // The entity is referred using a helper method of Datatable object + $andExpr->add($qb->expr()->eq($dataTable->getColumnDQLPartName('customer.isActive'), '1')); // Important to use 'andWhere' here... $qb->andWhere($andExpr); @@ -174,10 +201,12 @@ public function getDatatableAction() } ``` -As noted above, all join names are done by using CamelCase on the table name of the entity. Related -entities are separated out from the main entity with an underscore. So an entity relation on `Customer` -called `Location` with a field name called `city`, would be referenced in QueryBuilder as -`Customer_Location.city` +As noted above, you get join names using `getColumnDQLPartName` method of your $datable object. You just need +to pass as an argument the key used on your `mData` property or the key used by `addColumn` method (if you are +using `serverSideControl` to control columns). +So an entity relation on `Customer` called `Location` with a field name called `city`, would be retrieved in +QueryBuilder with `getColumnDQLPartName` method of `$datatable` object using `customer.location.city` as its +key. By default, pre-filtered results will return a total count back to DataTables.js with the filtering applied. If you would like the total count to reflect the total number of entities before the pre-filtering was applied @@ -198,9 +227,111 @@ public function getDatatableAction() } ``` +A shortcut method called `addWhereCollectionCallback` is also available to add a callback function with +`where` instructions collections in the end of `setWhere`, the main difference to the previous one +(`addWhereBuilderCallback`) is that the developer don't need to worry about the QueryBuilder and also reduces +the risk of disrupting something while its working with more developers. + +``` php + +public function getDatatableAction() +{ + $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer'); + + $dataTable->addColumnDQLPartName('customer.isActive'); + $dataTable->addColumnDQLPartName('customer.area'); + + $datatable->addWhereCollectionCallback(function($expr) use ($datatable) { + return array( // Important to return an array here... + $expr->eq( + $dataTable->getColumnDQLPartName('customer.isActive'), + '1' + ), + $expr->neq( + $dataTable->getColumnDQLPartName('customer.area'), + $expr->literal(Customer::AREA_AGRICULTURE) + ) + ); + }); + + return $datatable->getSearchResults(); +} +``` + +As noted above this simplifies the way of extending the query filtering. All you need to do is to return an +array of `Doctrine\ORM\Query\Expr` objects in your callback function. All this objects will be automatically +added to a `andX` collection and then inserted into the Datatable QueryBuilder using a `andWhere` clause. + +## Using Server Side Control + +By default the server side control is off, it means that the bundle will answer to every field request +from datatables.js. To change this behavior you first need to activate the server side control by providing +a second argument to `getDatatable` service method. The second step is to call `addColumn` method for every +desired information to be retrieved by datatables.js, let's consider the following: `description`, +`customer.lastname` and `customer.location.address`; So, the first argument is the attribute name using the +same entity relationship and property dotted notation as before. The second argument is an array map with +key => value pairs that represent some options of your column. It supports the raw notation of datables.js +`aoColumns` variable, but its recommended the use of some constant values to avoid mistakes. + +``` php + +public function getDatatableAction() +{ + // Notice the second argument, this will create a Datatable object server-side controlled + $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer', true); + $dataTable + ->addColumn( + 'customer.lastname', + array( + Datatable::COLUMN_TITLE => 'Last name' + ) + ) + ->addColumn( + 'description', + array( + Datatable::COLUMN_TITLE => 'Full description', + Datatable::COLUMN_SORTABLE => false + ) + ) + ->addColumn( + 'customer.location.address', + array( + Datatable::COLUMN_TITLE => 'Address', + Datatable::COLUMN_SORTABLE => false, + Datatable::COLUMN_SEARCHABLE => false + ) + ) + ; + + return $datatable->getSearchResults(); +} +``` + +The above code will not work without a minor modification in your front-end application, as explained before +this bundle uses the `mData` values of `aoColumns` to process all reqired data. Now, instead of forcing the +front-end developer to provide an entity level information, this data will be automatically returned through +`aoColumns` json object if the `serverSideControl` is set to true and if your columns are properly defined +inside your controllers actions. To make all of this happen just add a javascript on your code to make a +request to `sAjaxSource` before creating datatables.js instance, if you are using jQuery it will be something +like this: + +``` js +$(document).ready(function(){ + var sAjaxSource = 'http://URL_TO_GET_DATATABLE_ACTION'; + $.getJSON(sAjaxSource, function(dataTable){ + $('#example').dataTable({ + 'bProcessing': true, + 'bServerSide': true, + 'sAjaxSource': sAjaxSource, + 'aoColumns': dataTable.aoColumns + }); + }); +}); +``` + ## DateTime Formatting -All formatting is handled by the serializer service in use (likely JMSSerializer). To change the DateTime +All formatting is generally handled by the serializer service in use (likely JMSSerializer). To change the DateTime formatting when using the JMSSerializer you can either use annotation or define a default format in your `app/config/config.yml` file. @@ -233,6 +364,56 @@ use Doctrine\ORM\Mapping as ORM; For more details on formatting output, please refer to [this document](http://jmsyst.com/libs/serializer/master/reference/annotations). +## Column Post-filtering + +You can also use a callback function to filter values from your columns, using this approach you can define your own logic to deal with +any kind of formatting, translation (if not DB based), transformation, etc. You can filter any value/object to whatever you want before +they get returned to datatables.js instance in your application front-end. + +To use this feature you have two alternatives, the first one by using a third argument on `addColumn` method: + +```php +public function getDatatableAction() +{ + $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer', true); + $dataTable + ->addColumn( + 'customer.created', + array( + Datatable::COLUMN_TITLE => 'Created at' + ), + function ($v) { + return $v->format('m/d/Y H:i:s'); + } + ) + ; + + return $datatable->getSearchResults(); +} +``` + +And the second one using `addColumnFilter` method: + +```php +public function getDatatableAction() +{ + $datatable = $this->get('lankit_datatables')->getDatatable('AcmeDemoBundle:Customer', true); + $dataTable + ->addColumn( + 'customer.created', + array( + Datatable::COLUMN_TITLE => 'Created at' + ) + ) + ; + + $datatable->addColumnFilter('customer.created', function ($v) { + return $v->format('m/d/Y H:i:s'); + }); + return $datatable->getSearchResults(); +} +``` + ## DT_RowId and DT_RowClass The properties DT_RowId and DT_RowClass are special DataTables.js properties. See the following article...