This commit is contained in:
Manish Verma
2016-12-13 18:18:25 +05:30
parent fc98add11c
commit 2d8e640e9b
2314 changed files with 97798 additions and 75664 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace Yajra\Datatables\Contracts;
/**
* Interface DataTableButtonsContract.
*
* @package Yajra\Datatables\Contracts
* @author Arjay Angeles <aqangeles@gmail.com>
*/
interface DataTableButtonsContract
{
/**
* Export to excel file.
*
* @return mixed
*/
public function excel();
/**
* Export to CSV file.
*
* @return mixed
*/
public function csv();
/**
* Export to PDF file.
*
* @return mixed
*/
public function pdf();
/**
* Display printer friendly view.
*
* @return mixed
*/
public function printPreview();
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Yajra\Datatables\Contracts;
/**
* Interface DataTableContract
*
* @package Yajra\Datatables\Contracts
* @author Arjay Angeles <aqangeles@gmail.com>
*/
interface DataTableContract
{
/**
* Render view.
*
* @param $view
* @param array $data
* @param array $mergeData
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
*/
public function render($view, $data = [], $mergeData = []);
/**
* @return \Illuminate\Http\JsonResponse
*/
public function ajax();
/**
* @return \Yajra\Datatables\Html\Builder
*/
public function html();
/**
* @return \Yajra\Datatables\Html\Builder
*/
public function builder();
/**
* @return \Yajra\Datatables\Request
*/
public function request();
/**
* @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder|\Illuminate\Support\Collection
*/
public function query();
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Yajra\Datatables\Contracts;
/**
* Interface DataTableEngineContract
*
* @package Yajra\Datatables\Contracts
* @author Arjay Angeles <aqangeles@gmail.com>
*/
interface DataTableEngineContract
{
/**
* Get results.
*
* @return mixed
*/
public function results();
/**
* Count results.
*
* @return integer
*/
public function count();
/**
* Count total items.
*
* @return integer
*/
public function totalCount();
/**
* Set auto filter off and run your own filter.
* Overrides global search.
*
* @param \Closure $callback
* @param bool $globalSearch
* @return $this
*/
public function filter(\Closure $callback, $globalSearch = false);
/**
* Perform global search.
*
* @return void
*/
public function filtering();
/**
* Perform column search.
*
* @return void
*/
public function columnSearch();
/**
* Perform pagination.
*
* @return void
*/
public function paging();
/**
* Perform sorting of columns.
*
* @return void
*/
public function ordering();
/**
* Organizes works.
*
* @param bool $mDataSupport
* @param bool $orderFirst
* @return \Illuminate\Http\JsonResponse
*/
public function make($mDataSupport = false, $orderFirst = false);
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Yajra\Datatables\Contracts;
/**
* Interface DataTableScopeContract.
*
* @package Yajra\Datatables\Contracts
* @author Arjay Angeles <aqangeles@gmail.com>
*/
interface DataTableScopeContract
{
/**
* Apply a query scope.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query
* @return mixed
*/
public function apply($query);
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Yajra\Datatables;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
use Yajra\Datatables\Engines\CollectionEngine;
use Yajra\Datatables\Engines\EloquentEngine;
use Yajra\Datatables\Engines\QueryBuilderEngine;
/**
* Class Datatables.
*
* @package Yajra\Datatables
* @method EloquentEngine eloquent($builder)
* @method CollectionEngine collection(Collection $builder)
* @method QueryBuilderEngine queryBuilder(QueryBuilder $builder)
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Datatables
{
/**
* Datatables request object.
*
* @var \Yajra\Datatables\Request
*/
public $request;
/**
* Datatables builder.
*
* @var mixed
*/
public $builder;
/**
* Datatables constructor.
*
* @param \Yajra\Datatables\Request $request
*/
public function __construct(Request $request)
{
$this->request = $request->request->count() ? $request : Request::capture();
}
/**
* Gets query and returns instance of class.
*
* @param mixed $builder
* @return mixed
*/
public static function of($builder)
{
$datatables = app('Yajra\Datatables\Datatables');
$datatables->builder = $builder;
if ($builder instanceof QueryBuilder) {
$ins = $datatables->usingQueryBuilder($builder);
} else {
$ins = $builder instanceof Collection ? $datatables->usingCollection($builder) : $datatables->usingEloquent($builder);
}
return $ins;
}
/**
* Datatables using Query Builder.
*
* @param \Illuminate\Database\Query\Builder $builder
* @return \Yajra\Datatables\Engines\QueryBuilderEngine
*/
public function usingQueryBuilder(QueryBuilder $builder)
{
return new QueryBuilderEngine($builder, $this->request);
}
/**
* Datatables using Collection.
*
* @param \Illuminate\Support\Collection $builder
* @return \Yajra\Datatables\Engines\CollectionEngine
*/
public function usingCollection(Collection $builder)
{
return new CollectionEngine($builder, $this->request);
}
/**
* Datatables using Eloquent.
*
* @param mixed $builder
* @return \Yajra\Datatables\Engines\EloquentEngine
*/
public function usingEloquent($builder)
{
return new EloquentEngine($builder, $this->request);
}
/**
* Allows api call without the "using" word.
*
* @param string $name
* @param mixed $arguments
* @return $this|mixed
*/
public function __call($name, $arguments)
{
$name = 'using' . ucfirst($name);
if (method_exists($this, $name)) {
return call_user_func_array([$this, $name], $arguments);
}
return trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR);
}
/**
* Get html builder class.
*
* @return \Yajra\Datatables\Html\Builder
*/
public function getHtmlBuilder()
{
return app('Yajra\Datatables\Html\Builder');
}
/**
* Get request object.
*
* @return \Yajra\Datatables\Request|static
*/
public function getRequest()
{
return $this->request;
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Yajra\Datatables;
use Collective\Html\HtmlServiceProvider;
use Illuminate\Support\ServiceProvider;
use League\Fractal\Manager;
use League\Fractal\Serializer\DataArraySerializer;
use Maatwebsite\Excel\ExcelServiceProvider;
use Yajra\Datatables\Generators\DataTablesMakeCommand;
use Yajra\Datatables\Generators\DataTablesScopeCommand;
/**
* Class DatatablesServiceProvider.
*
* @package Yajra\Datatables
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class DatatablesServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot()
{
$this->loadViewsFrom(__DIR__ . '/resources/views', 'datatables');
$this->publishAssets();
$this->registerCommands();
}
/**
* Publish datatables assets.
*/
protected function publishAssets()
{
$this->publishes([
__DIR__ . '/config/config.php' => config_path('datatables.php'),
], 'datatables');
$this->publishes([
__DIR__ . '/resources/assets/buttons.server-side.js' => public_path('vendor/datatables/buttons.server-side.js'),
], 'datatables');
$this->publishes([
__DIR__ . '/resources/views' => base_path('/resources/views/vendor/datatables'),
], 'datatables');
}
/**
* Register datatables commands.
*/
protected function registerCommands()
{
$this->commands(DataTablesMakeCommand::class);
$this->commands(DataTablesScopeCommand::class);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
if ($this->isLumen()) {
require_once 'fallback.php';
}
$this->registerRequiredProviders();
$this->app->singleton('datatables', function () {
return new Datatables($this->app->make(Request::class));
});
$this->app->singleton('datatables.fractal', function () {
$fractal = new Manager;
$config = $this->app['config'];
$request = $this->app['request'];
$includesKey = $config->get('datatables.fractal.includes', 'include');
if ($request->get($includesKey)) {
$fractal->parseIncludes($request->get($includesKey));
}
$serializer = $config->get('datatables.fractal.serializer', DataArraySerializer::class);
$fractal->setSerializer(new $serializer);
return $fractal;
});
$this->registerAliases();
}
/**
* Check if app uses Lumen.
*
* @return bool
*/
protected function isLumen()
{
return str_contains($this->app->version(), 'Lumen');
}
/**
* Register 3rd party providers.
*/
protected function registerRequiredProviders()
{
$this->app->register(HtmlServiceProvider::class);
$this->app->register(ExcelServiceProvider::class);
}
/**
* Create aliases for the dependency.
*/
protected function registerAliases()
{
if (class_exists('Illuminate\Foundation\AliasLoader')) {
$loader = \Illuminate\Foundation\AliasLoader::getInstance();
$loader->alias('Datatables', \Yajra\Datatables\Facades\Datatables::class);
}
}
/**
* Get the services provided by the provider.
*
* @return string[]
*/
public function provides()
{
return ['datatables'];
}
}

View File

@@ -0,0 +1,967 @@
<?php
namespace Yajra\Datatables\Engines;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use League\Fractal\Resource\Collection;
use Yajra\Datatables\Contracts\DataTableEngineContract;
use Yajra\Datatables\Helper;
use Yajra\Datatables\Processors\DataProcessor;
/**
* Class BaseEngine.
*
* @package Yajra\Datatables\Engines
* @author Arjay Angeles <aqangeles@gmail.com>
*/
abstract class BaseEngine implements DataTableEngineContract
{
/**
* Datatables Request object.
*
* @var \Yajra\Datatables\Request
*/
public $request;
/**
* Database connection used.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* Builder object.
*
* @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
protected $query;
/**
* Query builder object.
*
* @var \Illuminate\Database\Query\Builder
*/
protected $builder;
/**
* Array of result columns/fields.
*
* @var array
*/
protected $columns = [];
/**
* DT columns definitions container (add/edit/remove/filter/order/escape).
*
* @var array
*/
protected $columnDef = [
'index' => false,
'append' => [],
'edit' => [],
'excess' => ['rn', 'row_num'],
'filter' => [],
'order' => [],
'escape' => [],
'blacklist' => ['password', 'remember_token'],
'whitelist' => '*',
];
/**
* Query type.
*
* @var string
*/
protected $query_type;
/**
* Extra/Added columns.
*
* @var array
*/
protected $extraColumns = [];
/**
* Total records.
*
* @var int
*/
protected $totalRecords = 0;
/**
* Total filtered records.
*
* @var int
*/
protected $filteredRecords = 0;
/**
* Auto-filter flag.
*
* @var bool
*/
protected $autoFilter = true;
/**
* Callback to override global search.
*
* @var \Closure
*/
protected $filterCallback;
/**
* Parameters to passed on filterCallback.
*
* @var mixed
*/
protected $filterCallbackParameters;
/**
* DT row templates container.
*
* @var array
*/
protected $templates = [
'DT_RowId' => '',
'DT_RowClass' => '',
'DT_RowData' => [],
'DT_RowAttr' => [],
];
/**
* Output transformer.
*
* @var \League\Fractal\TransformerAbstract
*/
protected $transformer = null;
/**
* Database prefix
*
* @var string
*/
protected $prefix;
/**
* Database driver used.
*
* @var string
*/
protected $database;
/**
* [internal] Track if any filter was applied for at least one column
*
* @var boolean
*/
protected $isFilterApplied = false;
/**
* Fractal serializer class.
*
* @var string|null
*/
protected $serializer = null;
/**
* Custom ordering callback.
*
* @var \Closure
*/
protected $orderCallback;
/**
* Array of data to append on json response.
*
* @var array
*/
private $appends = [];
/**
* Setup search keyword.
*
* @param string $value
* @return string
*/
public function setupKeyword($value)
{
if ($this->isSmartSearch()) {
$keyword = '%' . $value . '%';
if ($this->isWildcard()) {
$keyword = $this->wildcardLikeString($value);
}
// remove escaping slash added on js script request
$keyword = str_replace('\\', '%', $keyword);
return $keyword;
}
return $value;
}
/**
* Check if DataTables uses smart search.
*
* @return bool
*/
protected function isSmartSearch()
{
return Config::get('datatables.search.smart', true);
}
/**
* Get config use wild card status.
*
* @return bool
*/
public function isWildcard()
{
return Config::get('datatables.search.use_wildcards', false);
}
/**
* Adds % wildcards to the given string.
*
* @param string $str
* @param bool $lowercase
* @return string
*/
public function wildcardLikeString($str, $lowercase = true)
{
$wild = '%';
$length = Str::length($str);
if ($length) {
for ($i = 0; $i < $length; $i++) {
$wild .= $str[$i] . '%';
}
}
if ($lowercase) {
$wild = Str::lower($wild);
}
return $wild;
}
/**
* Will prefix column if needed.
*
* @param string $column
* @return string
*/
public function prefixColumn($column)
{
$table_names = $this->tableNames();
if (count(
array_filter($table_names, function ($value) use (&$column) {
return strpos($column, $value . '.') === 0;
})
)) {
// the column starts with one of the table names
$column = $this->prefix . $column;
}
return $column;
}
/**
* Will look through the query and all it's joins to determine the table names.
*
* @return array
*/
public function tableNames()
{
$names = [];
$query = $this->getQueryBuilder();
$names[] = $query->from;
$joins = $query->joins ?: [];
$databasePrefix = $this->prefix;
foreach ($joins as $join) {
$table = preg_split('/ as /i', $join->table);
$names[] = $table[0];
if (isset($table[1]) && ! empty($databasePrefix) && strpos($table[1], $databasePrefix) == 0) {
$names[] = preg_replace('/^' . $databasePrefix . '/', '', $table[1]);
}
}
return $names;
}
/**
* Get Query Builder object.
*
* @param mixed $instance
* @return mixed
*/
public function getQueryBuilder($instance = null)
{
if (! $instance) {
$instance = $this->query;
}
if ($this->isQueryBuilder()) {
return $instance;
}
return $instance->getQuery();
}
/**
* Check query type is a builder.
*
* @return bool
*/
public function isQueryBuilder()
{
return $this->query_type == 'builder';
}
/**
* Add column in collection.
*
* @param string $name
* @param string|callable $content
* @param bool|int $order
* @return $this
*/
public function addColumn($name, $content, $order = false)
{
$this->extraColumns[] = $name;
$this->columnDef['append'][] = ['name' => $name, 'content' => $content, 'order' => $order];
return $this;
}
/**
* Add DT row index column on response.
*
* @return $this
*/
public function addIndexColumn()
{
$this->columnDef['index'] = true;
return $this;
}
/**
* Edit column's content.
*
* @param string $name
* @param string|callable $content
* @return $this
*/
public function editColumn($name, $content)
{
$this->columnDef['edit'][] = ['name' => $name, 'content' => $content];
return $this;
}
/**
* Remove column from collection.
*
* @return $this
*/
public function removeColumn()
{
$names = func_get_args();
$this->columnDef['excess'] = array_merge($this->columnDef['excess'], $names);
return $this;
}
/**
* Declare columns to escape values.
*
* @param string|array $columns
* @return $this
*/
public function escapeColumns($columns = '*')
{
$this->columnDef['escape'] = $columns;
return $this;
}
/**
* Allows previous API calls where the methods were snake_case.
* Will convert a camelCase API call to a snake_case call.
* Allow query builder method to be used by the engine.
*
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
$name = Str::camel(Str::lower($name));
if (method_exists($this, $name)) {
return call_user_func_array([$this, $name], $arguments);
} elseif (method_exists($this->getQueryBuilder(), $name)) {
call_user_func_array([$this->getQueryBuilder(), $name], $arguments);
} else {
trigger_error('Call to undefined method ' . __CLASS__ . '::' . $name . '()', E_USER_ERROR);
}
return $this;
}
/**
* Sets DT_RowClass template.
* result: <tr class="output_from_your_template">.
*
* @param string|callable $content
* @return $this
*/
public function setRowClass($content)
{
$this->templates['DT_RowClass'] = $content;
return $this;
}
/**
* Sets DT_RowId template.
* result: <tr id="output_from_your_template">.
*
* @param string|callable $content
* @return $this
*/
public function setRowId($content)
{
$this->templates['DT_RowId'] = $content;
return $this;
}
/**
* Set DT_RowData templates.
*
* @param array $data
* @return $this
*/
public function setRowData(array $data)
{
$this->templates['DT_RowData'] = $data;
return $this;
}
/**
* Add DT_RowData template.
*
* @param string $key
* @param string|callable $value
* @return $this
*/
public function addRowData($key, $value)
{
$this->templates['DT_RowData'][$key] = $value;
return $this;
}
/**
* Set DT_RowAttr templates.
* result: <tr attr1="attr1" attr2="attr2">.
*
* @param array $data
* @return $this
*/
public function setRowAttr(array $data)
{
$this->templates['DT_RowAttr'] = $data;
return $this;
}
/**
* Add DT_RowAttr template.
*
* @param string $key
* @param string|callable $value
* @return $this
*/
public function addRowAttr($key, $value)
{
$this->templates['DT_RowAttr'][$key] = $value;
return $this;
}
/**
* Override default column filter search.
*
* @param string $column
* @param string|Closure $method
* @return $this
* @internal param $mixed ...,... All the individual parameters required for specified $method
* @internal string $1 Special variable that returns the requested search keyword.
*/
public function filterColumn($column, $method)
{
$params = func_get_args();
$this->columnDef['filter'][$column] = ['method' => $method, 'parameters' => array_splice($params, 2)];
return $this;
}
/**
* Order each given columns versus the given custom sql.
*
* @param array $columns
* @param string $sql
* @param array $bindings
* @return $this
*/
public function orderColumns(array $columns, $sql, $bindings = [])
{
foreach ($columns as $column) {
$this->orderColumn($column, str_replace(':column', $column, $sql), $bindings);
}
return $this;
}
/**
* Override default column ordering.
*
* @param string $column
* @param string $sql
* @param array $bindings
* @return $this
* @internal string $1 Special variable that returns the requested order direction of the column.
*/
public function orderColumn($column, $sql, $bindings = [])
{
$this->columnDef['order'][$column] = ['method' => 'orderByRaw', 'parameters' => [$sql, $bindings]];
return $this;
}
/**
* Set data output transformer.
*
* @param \League\Fractal\TransformerAbstract $transformer
* @return $this
*/
public function setTransformer($transformer)
{
$this->transformer = $transformer;
return $this;
}
/**
* Set fractal serializer class.
*
* @param string $serializer
* @return $this
*/
public function setSerializer($serializer)
{
$this->serializer = $serializer;
return $this;
}
/**
* Organizes works.
*
* @param bool $mDataSupport
* @param bool $orderFirst
* @return \Illuminate\Http\JsonResponse
*/
public function make($mDataSupport = false, $orderFirst = false)
{
$this->totalRecords = $this->totalCount();
if ($this->totalRecords) {
$this->orderRecords(! $orderFirst);
$this->filterRecords();
$this->orderRecords($orderFirst);
$this->paginate();
}
return $this->render($mDataSupport);
}
/**
* Sort records.
*
* @param boolean $skip
* @return void
*/
public function orderRecords($skip)
{
if (! $skip) {
$this->ordering();
}
}
/**
* Perform necessary filters.
*
* @return void
*/
public function filterRecords()
{
if ($this->autoFilter && $this->request->isSearchable()) {
$this->filtering();
}
if (is_callable($this->filterCallback)) {
call_user_func($this->filterCallback, $this->filterCallbackParameters);
}
$this->columnSearch();
$this->filteredRecords = $this->isFilterApplied ? $this->count() : $this->totalRecords;
}
/**
* Apply pagination.
*
* @return void
*/
public function paginate()
{
if ($this->request->isPaginationable()) {
$this->paging();
}
}
/**
* Render json response.
*
* @param bool $object
* @return \Illuminate\Http\JsonResponse
*/
public function render($object = false)
{
$output = array_merge([
'draw' => (int) $this->request['draw'],
'recordsTotal' => $this->totalRecords,
'recordsFiltered' => $this->filteredRecords,
], $this->appends);
if (isset($this->transformer)) {
$fractal = app('datatables.fractal');
if ($this->serializer) {
$fractal->setSerializer(new $this->serializer);
}
//Get transformer reflection
//Firs method parameter should be data/object to transform
$reflection = new \ReflectionMethod($this->transformer, 'transform');
$parameter = $reflection->getParameters()[0];
//If parameter is class assuming it requires object
//Else just pass array by default
if ($parameter->getClass()) {
$resource = new Collection($this->results(), $this->createTransformer());
} else {
$resource = new Collection(
$this->getProcessedData($object),
$this->createTransformer()
);
}
$collection = $fractal->createData($resource)->toArray();
$output['data'] = $collection['data'];
} else {
$output['data'] = Helper::transform($this->getProcessedData($object));
}
if ($this->isDebugging()) {
$output = $this->showDebugger($output);
}
return new JsonResponse($output);
}
/**
* Get or create transformer instance.
*
* @return \League\Fractal\TransformerAbstract
*/
protected function createTransformer()
{
if ($this->transformer instanceof \League\Fractal\TransformerAbstract) {
return $this->transformer;
}
return new $this->transformer();
}
/**
* Get processed data
*
* @param bool|false $object
* @return array
*/
private function getProcessedData($object = false)
{
$processor = new DataProcessor(
$this->results(),
$this->columnDef,
$this->templates,
$this->request['start']
);
return $processor->process($object);
}
/**
* Check if app is in debug mode.
*
* @return bool
*/
public function isDebugging()
{
return Config::get('app.debug', false);
}
/**
* Append debug parameters on output.
*
* @param array $output
* @return array
*/
public function showDebugger(array $output)
{
$output['queries'] = $this->connection->getQueryLog();
$output['input'] = $this->request->all();
return $output;
}
/**
* Update flags to disable global search
*
* @param \Closure $callback
* @param mixed $parameters
* @param bool $autoFilter
*/
public function overrideGlobalSearch(\Closure $callback, $parameters, $autoFilter = false)
{
$this->autoFilter = $autoFilter;
$this->isFilterApplied = true;
$this->filterCallback = $callback;
$this->filterCallbackParameters = $parameters;
}
/**
* Get config is case insensitive status.
*
* @return bool
*/
public function isCaseInsensitive()
{
return Config::get('datatables.search.case_insensitive', false);
}
/**
* Append data on json response.
*
* @param mixed $key
* @param mixed $value
* @return $this
*/
public function with($key, $value = '')
{
if (is_array($key)) {
$this->appends = $key;
} elseif (is_callable($value)) {
$this->appends[$key] = value($value);
} else {
$this->appends[$key] = value($value);
}
return $this;
}
/**
* Override default ordering method with a closure callback.
*
* @param \Closure $closure
* @return $this
*/
public function order(\Closure $closure)
{
$this->orderCallback = $closure;
return $this;
}
/**
* Update list of columns that is not allowed for search/sort.
*
* @param array $blacklist
* @return $this
*/
public function blacklist(array $blacklist)
{
$this->columnDef['blacklist'] = $blacklist;
return $this;
}
/**
* Update list of columns that is not allowed for search/sort.
*
* @param string|array $whitelist
* @return $this
*/
public function whitelist($whitelist = '*')
{
$this->columnDef['whitelist'] = $whitelist;
return $this;
}
/**
* Set smart search config at runtime.
*
* @param bool $bool
* @return $this
*/
public function smart($bool = true)
{
Config::set('datatables.search.smart', $bool);
return $this;
}
/**
* Check if column is blacklisted.
*
* @param string $column
* @return bool
*/
protected function isBlacklisted($column)
{
if (in_array($column, $this->columnDef['blacklist'])) {
return true;
}
if ($this->columnDef['whitelist'] === '*' || in_array($column, $this->columnDef['whitelist'])) {
return false;
}
return true;
}
/**
* Get column name to be use for filtering and sorting.
*
* @param integer $index
* @param bool $wantsAlias
* @return string
*/
protected function getColumnName($index, $wantsAlias = false)
{
$column = $this->request->columnName($index);
// DataTables is using make(false)
if (is_numeric($column)) {
$column = $this->getColumnNameByIndex($index);
}
if (Str::contains(Str::upper($column), ' AS ')) {
$column = $this->extractColumnName($column, $wantsAlias);
}
return $column;
}
/**
* Get column name by order column index.
*
* @param int $index
* @return mixed
*/
protected function getColumnNameByIndex($index)
{
$name = isset($this->columns[$index]) && $this->columns[$index] <> '*' ? $this->columns[$index] : $this->getPrimaryKeyName();
return in_array($name, $this->extraColumns, true) ? $this->getPrimaryKeyName() : $name;
}
/**
* If column name could not be resolved then use primary key.
*
* @return string
*/
protected function getPrimaryKeyName()
{
if ($this->isEloquent()) {
return $this->query->getModel()->getKeyName();
}
return 'id';
}
/**
* Check if the engine used was eloquent.
*
* @return bool
*/
protected function isEloquent()
{
return $this->query_type === 'eloquent';
}
/**
* Get column name from string.
*
* @param string $str
* @param bool $wantsAlias
* @return string
*/
protected function extractColumnName($str, $wantsAlias)
{
$matches = explode(' as ', Str::lower($str));
if (! empty($matches)) {
if ($wantsAlias) {
return array_pop($matches);
} else {
return array_shift($matches);
}
} elseif (strpos($str, '.')) {
$array = explode('.', $str);
return array_pop($array);
}
return $str;
}
/**
* Check if the current sql language is based on oracle syntax.
*
* @return bool
*/
protected function isOracleSql()
{
return in_array($this->database, ['oracle', 'oci8']);
}
/**
* Set total records manually.
*
* @param int $total
* @return $this
*/
public function setTotalRecords($total)
{
$this->totalRecords = $total;
return $this;
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace Yajra\Datatables\Engines;
use Closure;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Yajra\Datatables\Request;
/**
* Class CollectionEngine.
*
* @package Yajra\Datatables\Engines
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class CollectionEngine extends BaseEngine
{
/**
* Collection object
*
* @var \Illuminate\Support\Collection
*/
public $collection;
/**
* Collection object
*
* @var \Illuminate\Support\Collection
*/
public $original_collection;
/**
* CollectionEngine constructor.
*
* @param \Illuminate\Support\Collection $collection
* @param \Yajra\Datatables\Request $request
*/
public function __construct(Collection $collection, Request $request)
{
$this->request = $request;
$this->collection = $collection;
$this->original_collection = $collection;
$this->columns = array_keys($this->serialize($collection->first()));
}
/**
* Serialize collection
*
* @param mixed $collection
* @return mixed|null
*/
protected function serialize($collection)
{
return $collection instanceof Arrayable ? $collection->toArray() : (array) $collection;
}
/**
* Set auto filter off and run your own filter.
* Overrides global search.
*
* @param \Closure $callback
* @param bool $globalSearch
* @return $this
*/
public function filter(Closure $callback, $globalSearch = false)
{
$this->overrideGlobalSearch($callback, $this, $globalSearch);
return $this;
}
/**
* Append debug parameters on output.
*
* @param array $output
* @return array
*/
public function showDebugger(array $output)
{
$output["input"] = $this->request->all();
return $output;
}
/**
* Count total items.
*
* @return integer
*/
public function totalCount()
{
return $this->count();
}
/**
* Count results.
*
* @return integer
*/
public function count()
{
return $this->collection->count();
}
/**
* Perform sorting of columns.
*
* @return void
*/
public function ordering()
{
if ($this->orderCallback) {
call_user_func($this->orderCallback, $this);
return;
}
foreach ($this->request->orderableColumns() as $orderable) {
$column = $this->getColumnName($orderable['column']);
$this->collection = $this->collection->sortBy(
function ($row) use ($column) {
$data = $this->serialize($row);
return Arr::get($data, $column);
}
);
if ($orderable['direction'] == 'desc') {
$this->collection = $this->collection->reverse();
}
}
}
/**
* Perform global search.
*
* @return void
*/
public function filtering()
{
$columns = $this->request['columns'];
$this->collection = $this->collection->filter(
function ($row) use ($columns) {
$data = $this->serialize($row);
$this->isFilterApplied = true;
$found = [];
$keyword = $this->request->keyword();
foreach ($this->request->searchableColumnIndex() as $index) {
$column = $this->getColumnName($index);
if (! $value = Arr::get($data, $column)) {
continue;
}
if ($this->isCaseInsensitive()) {
$found[] = Str::contains(Str::lower($value), Str::lower($keyword));
} else {
$found[] = Str::contains($value, $keyword);
}
}
return in_array(true, $found);
}
);
}
/**
* Perform column search.
*
* @return void
*/
public function columnSearch()
{
$columns = $this->request->get('columns');
for ($i = 0, $c = count($columns); $i < $c; $i++) {
if ($this->request->isColumnSearchable($i)) {
$this->isFilterApplied = true;
$column = $this->getColumnName($i);
$keyword = $this->request->columnKeyword($i);
$this->collection = $this->collection->filter(
function ($row) use ($column, $keyword) {
$data = $this->serialize($row);
$value = Arr::get($data, $column);
if ($this->isCaseInsensitive()) {
return strpos(Str::lower($value), Str::lower($keyword)) !== false;
} else {
return strpos($value, $keyword) !== false;
}
}
);
}
}
}
/**
* Perform pagination.
*
* @return void
*/
public function paging()
{
$this->collection = $this->collection->slice(
$this->request['start'],
(int) $this->request['length'] > 0 ? $this->request['length'] : 10
);
}
/**
* Get results.
*
* @return mixed
*/
public function results()
{
return $this->collection->all();
}
/**
* Organizes works.
*
* @param bool $mDataSupport
* @param bool $orderFirst
* @return \Illuminate\Http\JsonResponse
*/
public function make($mDataSupport = false, $orderFirst = true)
{
return parent::make($mDataSupport, $orderFirst);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Yajra\Datatables\Engines;
use Illuminate\Database\Eloquent\Builder;
use Yajra\Datatables\Request;
/**
* Class EloquentEngine.
*
* @package Yajra\Datatables\Engines
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class EloquentEngine extends QueryBuilderEngine
{
/**
* @param mixed $model
* @param \Yajra\Datatables\Request $request
*/
public function __construct($model, Request $request)
{
$builder = $model instanceof Builder ? $model : $model->getQuery();
parent::__construct($builder->getQuery(), $request);
$this->query = $builder;
$this->query_type = 'eloquent';
}
}

View File

@@ -0,0 +1,535 @@
<?php
namespace Yajra\Datatables\Engines;
use Closure;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Str;
use Yajra\Datatables\Helper;
use Yajra\Datatables\Request;
/**
* Class QueryBuilderEngine.
*
* @package Yajra\Datatables\Engines
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class QueryBuilderEngine extends BaseEngine
{
/**
* @param \Illuminate\Database\Query\Builder $builder
* @param \Yajra\Datatables\Request $request
*/
public function __construct(Builder $builder, Request $request)
{
$this->query = $builder;
$this->init($request, $builder);
}
/**
* Initialize attributes.
*
* @param \Yajra\Datatables\Request $request
* @param \Illuminate\Database\Query\Builder $builder
* @param string $type
*/
protected function init($request, $builder, $type = 'builder')
{
$this->request = $request;
$this->query_type = $type;
$this->columns = $builder->columns;
$this->connection = $builder->getConnection();
$this->prefix = $this->connection->getTablePrefix();
$this->database = $this->connection->getDriverName();
if ($this->isDebugging()) {
$this->connection->enableQueryLog();
}
}
/**
* Set auto filter off and run your own filter.
* Overrides global search
*
* @param \Closure $callback
* @param bool $globalSearch
* @return $this
*/
public function filter(Closure $callback, $globalSearch = false)
{
$this->overrideGlobalSearch($callback, $this->query, $globalSearch);
return $this;
}
/**
* Organizes works
*
* @param bool $mDataSupport
* @param bool $orderFirst
* @return \Illuminate\Http\JsonResponse
*/
public function make($mDataSupport = false, $orderFirst = false)
{
return parent::make($mDataSupport, $orderFirst);
}
/**
* Count total items.
*
* @return integer
*/
public function totalCount()
{
return $this->totalRecords ? $this->totalRecords : $this->count();
}
/**
* Counts current query.
*
* @return int
*/
public function count()
{
$myQuery = clone $this->query;
// if its a normal query ( no union, having and distinct word )
// replace the select with static text to improve performance
if (! Str::contains(Str::lower($myQuery->toSql()), ['union', 'having', 'distinct', 'order by', 'group by'])) {
$row_count = $this->wrap('row_count');
$myQuery->select($this->connection->raw("'1' as {$row_count}"));
}
return $this->connection->table($this->connection->raw('(' . $myQuery->toSql() . ') count_row_table'))
->setBindings($myQuery->getBindings())->count();
}
/**
* Wrap column with DB grammar.
*
* @param string $column
* @return string
*/
protected function wrap($column) {
return $this->connection->getQueryGrammar()->wrap($column);
}
/**
* Perform global search.
*
* @return void
*/
public function filtering()
{
$this->query->where(
function ($query) {
$globalKeyword = $this->request->keyword();
$queryBuilder = $this->getQueryBuilder($query);
foreach ($this->request->searchableColumnIndex() as $index) {
$columnName = $this->getColumnName($index);
if ($this->isBlacklisted($columnName)) {
continue;
}
// check if custom column filtering is applied
if (isset($this->columnDef['filter'][$columnName])) {
$columnDef = $this->columnDef['filter'][$columnName];
// check if global search should be applied for the specific column
$applyGlobalSearch = count($columnDef['parameters']) == 0 || end($columnDef['parameters']) !== false;
if (! $applyGlobalSearch) {
continue;
}
if ($columnDef['method'] instanceof Closure) {
$whereQuery = $queryBuilder->newQuery();
call_user_func_array($columnDef['method'], [$whereQuery, $globalKeyword]);
$queryBuilder->addNestedWhereQuery($whereQuery, 'or');
} else {
$this->compileColumnQuery(
$queryBuilder,
Helper::getOrMethod($columnDef['method']),
$columnDef['parameters'],
$columnName,
$globalKeyword
);
}
} else {
if (count(explode('.', $columnName)) > 1) {
$eagerLoads = $this->getEagerLoads();
$parts = explode('.', $columnName);
$relationColumn = array_pop($parts);
$relation = implode('.', $parts);
if (in_array($relation, $eagerLoads)) {
$this->compileRelationSearch(
$queryBuilder,
$relation,
$relationColumn,
$globalKeyword
);
} else {
$this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword);
}
} else {
$this->compileQuerySearch($queryBuilder, $columnName, $globalKeyword);
}
}
$this->isFilterApplied = true;
}
}
);
}
/**
* Perform filter column on selected field.
*
* @param mixed $query
* @param string|Closure $method
* @param mixed $parameters
* @param string $column
* @param string $keyword
*/
protected function compileColumnQuery($query, $method, $parameters, $column, $keyword)
{
if (method_exists($query, $method)
&& count($parameters) <= with(new \ReflectionMethod($query, $method))->getNumberOfParameters()
) {
if (Str::contains(Str::lower($method), 'raw')
|| Str::contains(Str::lower($method), 'exists')
) {
call_user_func_array(
[$query, $method],
$this->parameterize($parameters, $keyword)
);
} else {
call_user_func_array(
[$query, $method],
$this->parameterize($column, $parameters, $keyword)
);
}
}
}
/**
* Build Query Builder Parameters.
*
* @return array
*/
protected function parameterize()
{
$args = func_get_args();
$keyword = count($args) > 2 ? $args[2] : $args[1];
$parameters = Helper::buildParameters($args);
$parameters = Helper::replacePatternWithKeyword($parameters, $keyword, '$1');
return $parameters;
}
/**
* Get eager loads keys if eloquent.
*
* @return array
*/
protected function getEagerLoads()
{
if ($this->query_type == 'eloquent') {
return array_keys($this->query->getEagerLoads());
}
return [];
}
/**
* Add relation query on global search.
*
* @param mixed $query
* @param string $relation
* @param string $column
* @param string $keyword
*/
protected function compileRelationSearch($query, $relation, $column, $keyword)
{
$myQuery = clone $this->query;
$myQuery->orWhereHas($relation, function ($builder) use ($column, $keyword, $query) {
$builder->select($this->connection->raw('count(1)'));
$this->compileQuerySearch($builder, $column, $keyword, '');
$builder = "({$builder->toSql()}) >= 1";
$query->orWhereRaw($builder, [$keyword]);
});
}
/**
* Compile query builder where clause depending on configurations.
*
* @param mixed $query
* @param string $column
* @param string $keyword
* @param string $relation
*/
protected function compileQuerySearch($query, $column, $keyword, $relation = 'or')
{
$column = strstr($column, '(') ? $this->connection->raw($column) : $column;
$column = $this->castColumn($column);
$sql = $column . ' LIKE ?';
if ($this->isCaseInsensitive()) {
$sql = 'LOWER(' . $column . ') LIKE ?';
$keyword = Str::lower($keyword);
}
if ($this->isWildcard()) {
$keyword = $this->wildcardLikeString($keyword);
}
if ($this->isSmartSearch()) {
$keyword = "%$keyword%";
}
$query->{$relation .'WhereRaw'}($sql, [$keyword]);
}
/**
* Wrap a column and cast in pgsql.
*
* @param string $column
* @return string
*/
public function castColumn($column)
{
$column = $this->wrap($column);
if ($this->database === 'pgsql') {
$column = 'CAST(' . $column . ' as TEXT)';
} elseif ($this->database === 'firebird') {
$column = 'CAST(' . $column . ' as VARCHAR(255))';
}
return $column;
}
/**
* Perform column search.
*
* @return void
*/
public function columnSearch()
{
$columns = $this->request->get('columns', []);
foreach ($columns as $index => $column) {
if (! $this->request->isColumnSearchable($index)) {
continue;
}
$column = $this->getColumnName($index);
if (isset($this->columnDef['filter'][$column])) {
$columnDef = $this->columnDef['filter'][$column];
// get a raw keyword (without wildcards)
$keyword = $this->getSearchKeyword($index, true);
$builder = $this->getQueryBuilder();
if ($columnDef['method'] instanceof Closure) {
$whereQuery = $builder->newQuery();
call_user_func_array($columnDef['method'], [$whereQuery, $keyword]);
$builder->addNestedWhereQuery($whereQuery);
} else {
$this->compileColumnQuery(
$builder,
$columnDef['method'],
$columnDef['parameters'],
$column,
$keyword
);
}
} else {
if (count(explode('.', $column)) > 1) {
$eagerLoads = $this->getEagerLoads();
$parts = explode('.', $column);
$relationColumn = array_pop($parts);
$relation = implode('.', $parts);
if (in_array($relation, $eagerLoads)) {
$column = $this->joinEagerLoadedColumn($relation, $relationColumn);
}
}
$keyword = $this->getSearchKeyword($index);
$this->compileColumnSearch($index, $column, $keyword);
}
$this->isFilterApplied = true;
}
}
/**
* Get proper keyword to use for search.
*
* @param int $i
* @param bool $raw
* @return string
*/
private function getSearchKeyword($i, $raw = false)
{
$keyword = $this->request->columnKeyword($i);
if ($raw || $this->request->isRegex($i)) {
return $keyword;
}
return $this->setupKeyword($keyword);
}
/**
* Compile queries for column search.
*
* @param int $i
* @param mixed $column
* @param string $keyword
*/
protected function compileColumnSearch($i, $column, $keyword)
{
if ($this->request->isRegex($i)) {
$column = strstr($column, '(') ? $this->connection->raw($column) : $column;
$this->regexColumnSearch($column, $keyword);
} else {
$this->compileQuerySearch($this->query, $column, $keyword, '');
}
}
/**
* Compile regex query column search.
*
* @param mixed $column
* @param string $keyword
*/
protected function regexColumnSearch($column, $keyword)
{
if ($this->isOracleSql()) {
$sql = ! $this->isCaseInsensitive() ? 'REGEXP_LIKE( ' . $column . ' , ? )' : 'REGEXP_LIKE( LOWER(' . $column . ') , ?, \'i\' )';
$this->query->whereRaw($sql, [$keyword]);
} else {
$sql = ! $this->isCaseInsensitive() ? $column . ' REGEXP ?' : 'LOWER(' . $column . ') REGEXP ?';
$this->query->whereRaw($sql, [Str::lower($keyword)]);
}
}
/**
* Perform sorting of columns.
*
* @return void
*/
public function ordering()
{
if ($this->orderCallback) {
call_user_func($this->orderCallback, $this->getQueryBuilder());
return;
}
foreach ($this->request->orderableColumns() as $orderable) {
$column = $this->getColumnName($orderable['column'], true);
if ($this->isBlacklisted($column)) {
continue;
}
if (isset($this->columnDef['order'][$column])) {
$method = $this->columnDef['order'][$column]['method'];
$parameters = $this->columnDef['order'][$column]['parameters'];
$this->compileColumnQuery(
$this->getQueryBuilder(),
$method,
$parameters,
$column,
$orderable['direction']
);
} else {
if (count(explode('.', $column)) > 1) {
$eagerLoads = $this->getEagerLoads();
$parts = explode('.', $column);
$relationColumn = array_pop($parts);
$relation = implode('.', $parts);
if (in_array($relation, $eagerLoads)) {
$column = $this->joinEagerLoadedColumn($relation, $relationColumn);
}
}
$this->getQueryBuilder()->orderBy($column, $orderable['direction']);
}
}
}
/**
* Join eager loaded relation and get the related column name.
*
* @param string $relation
* @param string $relationColumn
* @return string
*/
protected function joinEagerLoadedColumn($relation, $relationColumn)
{
$joins = [];
foreach ((array) $this->getQueryBuilder()->joins as $key => $join) {
$joins[] = $join->table;
}
$model = $this->query->getRelation($relation);
if ($model instanceof BelongsToMany) {
$pivot = $model->getTable();
$pivotPK = $model->getForeignKey();
$pivotFK = $model->getQualifiedParentKeyName();
if (! in_array($pivot, $joins)) {
$this->getQueryBuilder()->leftJoin($pivot, $pivotPK, '=', $pivotFK);
}
$related = $model->getRelated();
$table = $related->getTable();
$tablePK = $related->getForeignKey();
$tableFK = $related->getQualifiedKeyName();
if (! in_array($table, $joins)) {
$this->getQueryBuilder()->leftJoin($table, $pivot . '.' . $tablePK, '=', $tableFK);
}
} else {
$table = $model->getRelated()->getTable();
if ($model instanceof HasOne) {
$foreign = $model->getForeignKey();
$other = $model->getQualifiedParentKeyName();
} else {
$foreign = $model->getQualifiedForeignKey();
$other = $model->getQualifiedOtherKeyName();
}
if (! in_array($table, $joins)) {
$this->getQueryBuilder()->leftJoin($table, $foreign, '=', $other);
}
}
$column = $table . '.' . $relationColumn;
return $column;
}
/**
* Perform pagination
*
* @return void
*/
public function paging()
{
$this->query->skip($this->request['start'])
->take((int) $this->request['length'] > 0 ? $this->request['length'] : 10);
}
/**
* Get results
*
* @return array|static[]
*/
public function results()
{
return $this->query->get();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Yajra\Datatables\Facades;
use Illuminate\Support\Facades\Facade;
/**
* Class Datatables.
*
* @package Yajra\Datatables\Facades
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Datatables extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'datatables';
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Yajra\Datatables\Generators;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;
/**
* Class DataTablesMakeCommand.
*
* @package Yajra\Datatables\Generators
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class DataTablesMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'datatables:make';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new DataTable service class.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'DataTable';
/**
* The model class to be used by dataTable.
*
* @var string
*/
protected $model;
/**
* DataTable export filename.
*
* @var string
*/
protected $filename;
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*/
protected function buildClass($name)
{
$stub = $this->files->get($this->getStub());
$stub = $this->replaceNamespace($stub, $name)->replaceClass($stub, $name);
return $this->replaceModelImport($stub)->replaceModel($stub)->replaceFilename($stub);
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return __DIR__ . '/stubs/datatables.stub';
}
/**
* Replace model name.
*
* @param string $stub
* @return mixed
*/
protected function replaceModel(&$stub)
{
$model = explode('\\', $this->model);
$model = array_pop($model);
$stub = str_replace('ModelName', $model, $stub);
return $this;
}
/**
* Replace model import.
*
* @param string $stub
* @return $this
*/
protected function replaceModelImport(&$stub)
{
$stub = str_replace(
'DummyModel', str_replace('\\\\', '\\', $this->model), $stub
);
return $this;
}
/**
* Replace the filename.
*
* @param string $stub
* @return string
*/
protected function replaceFilename(&$stub)
{
$stub = str_replace(
'DummyFilename', $this->filename, $stub
);
return $stub;
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['model', null, InputOption::VALUE_NONE, 'Use the provided name as the model.', null],
];
}
/**
* Determine if the class already exists.
*
* @param string $rawName
* @return bool
*/
protected function alreadyExists($rawName)
{
$name = $this->parseName($rawName);
$this->setModel($rawName);
$this->setFilename($rawName);
return $this->files->exists($this->getPath($name));
}
/**
* Parse the name and format according to the root namespace.
*
* @param string $name
* @return string
*/
protected function parseName($name)
{
$rootNamespace = $this->laravel->getNamespace();
if (Str::startsWith($name, $rootNamespace)) {
return $name;
}
if (Str::contains($name, '/')) {
$name = str_replace('/', '\\', $name);
}
if (! Str::contains(Str::lower($name), 'datatable')) {
$name .= 'DataTable';
}
return $this->parseName($this->getDefaultNamespace(trim($rootNamespace, '\\')) . '\\' . $name);
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace . "\\" . $this->laravel['config']->get('datatables.namespace.base', 'DataTables');
}
/**
* Set the model to be used.
*
* @param string $name
*/
protected function setModel($name)
{
$rootNamespace = $this->laravel->getNamespace();
$modelNamespace = $this->laravel['config']->get('datatables.namespace.model');
$this->model = $this->option('model')
? $rootNamespace . "\\" . ($modelNamespace ? $modelNamespace . "\\" : "") . $name
: $rootNamespace . "\\User";
}
/**
* Set the filename for export.
*
* @param string $name
*/
protected function setFilename($name)
{
$this->filename = Str::lower(Str::plural($name));
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Yajra\Datatables\Generators;
use Illuminate\Console\GeneratorCommand;
/**
* Class DataTablesScopeCommand.
*
* @package Yajra\Datatables\Generators
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class DataTablesScopeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'datatables:scope';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new DataTable Scope class.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'DataTable Scope';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace . '\DataTables\Scopes';
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return __DIR__ . '/stubs/scopes.stub';
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace DummyNamespace;
use DummyModel;
use Yajra\Datatables\Services\DataTable;
class DummyClass extends DataTable
{
/**
* Display ajax response.
*
* @return \Illuminate\Http\JsonResponse
*/
public function ajax()
{
return $this->datatables
->eloquent($this->query())
->addColumn('action', 'path.to.action.view')
->make(true);
}
/**
* Get the query object to be processed by dataTables.
*
* @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder|\Illuminate\Support\Collection
*/
public function query()
{
$query = ModelName::query();
return $this->applyScopes($query);
}
/**
* Optional method if you want to use html builder.
*
* @return \Yajra\Datatables\Html\Builder
*/
public function html()
{
return $this->builder()
->columns($this->getColumns())
->ajax('')
->addAction(['width' => '80px'])
->parameters($this->getBuilderParameters());
}
/**
* Get columns.
*
* @return array
*/
protected function getColumns()
{
return [
'id',
// add your columns
'created_at',
'updated_at',
];
}
/**
* Get filename for export.
*
* @return string
*/
protected function filename()
{
return 'DummyFilename_' . time();
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace DummyNamespace;
use Yajra\Datatables\Contracts\DataTableScopeContract;
class DummyClass implements DataTableScopeContract
{
/**
* Apply a query scope.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query
* @return mixed
*/
public function apply($query)
{
// return $query->where('id', 1);
}
}

View File

@@ -0,0 +1,313 @@
<?php
namespace Yajra\Datatables;
use DateTime;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Illuminate\View\Compilers\BladeCompiler;
/**
* Class Helper.
*
* @package Yajra\Datatables
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Helper
{
/**
* Places item of extra columns into results by care of their order.
*
* @param array $item
* @param array $array
* @return array
*/
public static function includeInArray($item, $array)
{
if (self::isItemOrderInvalid($item, $array)) {
return array_merge($array, [$item['name'] => $item['content']]);
} else {
$count = 0;
$last = $array;
$first = [];
foreach ($array as $key => $value) {
if ($count == $item['order']) {
return array_merge($first, [$item['name'] => $item['content']], $last);
}
unset($last[$key]);
$first[$key] = $value;
$count++;
}
}
}
/**
* Check if item order is valid.
*
* @param array $item
* @param array $array
* @return bool
*/
protected static function isItemOrderInvalid($item, $array)
{
return $item['order'] === false || $item['order'] >= count($array);
}
/**
* Determines if content is callable or blade string, processes and returns.
*
* @param string|callable $content Pre-processed content
* @param array $data data to use with blade template
* @param mixed $param parameter to call with callable
* @return string Processed content
*/
public static function compileContent($content, array $data, $param)
{
if (is_string($content)) {
return static::compileBlade($content, static::getMixedValue($data, $param));
} elseif (is_callable($content)) {
return $content($param);
}
return $content;
}
/**
* Parses and compiles strings by using Blade Template System.
*
* @param string $str
* @param array $data
* @return string
* @throws \Exception
*/
public static function compileBlade($str, $data = [])
{
if (view()->exists($str)) {
return view($str, $data)->render();
}
$empty_filesystem_instance = new Filesystem();
$blade = new BladeCompiler($empty_filesystem_instance, 'datatables');
$parsed_string = $blade->compileString($str);
ob_start() && extract($data, EXTR_SKIP);
try {
eval('?>' . $parsed_string);
} catch (\Exception $e) {
ob_end_clean();
throw $e;
}
$str = ob_get_contents();
ob_end_clean();
return $str;
}
/**
* Get a mixed value of custom data and the parameters.
*
* @param array $data
* @param mixed $param
* @return array
*/
public static function getMixedValue(array $data, $param)
{
$param = self::castToArray($param);
foreach ($data as $key => $value) {
if (isset($param[$key])) {
$data[$key] = $param[$key];
}
}
return $data;
}
/**
* Cast the parameter into an array.
*
* @param mixed $param
* @return array
*/
public static function castToArray($param)
{
if ($param instanceof \stdClass) {
$param = (array) $param;
return $param;
}
if ($param instanceof Arrayable) {
return $param->toArray();
}
return $param;
}
/**
* Get equivalent or method of query builder.
*
* @param string $method
* @return string
*/
public static function getOrMethod($method)
{
if (! Str::contains(Str::lower($method), 'or')) {
return 'or' . ucfirst($method);
}
return $method;
}
/**
* Wrap value depending on database type.
*
* @param string $database
* @param string $value
* @return string
*/
public static function wrapDatabaseValue($database, $value)
{
$parts = explode('.', $value);
$column = '';
foreach ($parts as $key) {
$column = static::wrapDatabaseColumn($database, $key, $column);
}
return substr($column, 0, strlen($column) - 1);
}
/**
* Database column wrapper.
*
* @param string $database
* @param string $key
* @param string $column
* @return string
*/
public static function wrapDatabaseColumn($database, $key, $column)
{
switch ($database) {
case 'mysql':
$column .= '`' . str_replace('`', '``', $key) . '`' . '.';
break;
case 'sqlsrv':
$column .= '[' . str_replace(']', ']]', $key) . ']' . '.';
break;
case 'pgsql':
case 'sqlite':
$column .= '"' . str_replace('"', '""', $key) . '"' . '.';
break;
default:
$column .= $key . '.';
}
return $column;
}
/**
* Converts array object values to associative array.
*
* @param mixed $row
* @return array
*/
public static function convertToArray($row)
{
$data = $row instanceof Arrayable ? $row->toArray() : (array) $row;
foreach (array_keys($data) as $key) {
if (is_object($data[$key]) || is_array($data[$key])) {
$data[$key] = self::convertToArray($data[$key]);
}
}
return $data;
}
/**
* @param array $data
* @return array
*/
public static function transform(array $data)
{
return array_map(function ($row) {
return self::transformRow($row);
}, $data);
}
/**
* Transform row data into an array.
*
* @param mixed $row
* @return array
*/
protected static function transformRow($row)
{
foreach ($row as $key => $value) {
if ($value instanceof DateTime) {
$row[$key] = $value->format('Y-m-d H:i:s');
} else {
if (is_object($value)) {
$row[$key] = (string) $value;
} else {
$row[$key] = $value;
}
}
}
return $row;
}
/**
* Build parameters depending on # of arguments passed.
*
* @param array $args
* @return array
*/
public static function buildParameters(array $args)
{
$parameters = [];
if (count($args) > 2) {
$parameters[] = $args[0];
foreach ($args[1] as $param) {
$parameters[] = $param;
}
} else {
foreach ($args[0] as $param) {
$parameters[] = $param;
}
}
return $parameters;
}
/**
* Replace all pattern occurrences with keyword
*
* @param array $subject
* @param string $keyword
* @param string $pattern
* @return array
*/
public static function replacePatternWithKeyword(array $subject, $keyword, $pattern = '$1')
{
$parameters = [];
foreach ($subject as $param) {
if (is_array($param)) {
$parameters[] = self::replacePatternWithKeyword($param, $keyword, $pattern);
} else {
$parameters[] = str_replace($pattern, $keyword, $param);
}
}
return $parameters;
}
}

View File

@@ -0,0 +1,620 @@
<?php
namespace Yajra\Datatables\Html;
use Collective\Html\FormBuilder;
use Collective\Html\HtmlBuilder;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\View\Factory;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
/**
* Class Builder.
*
* @package Yajra\Datatables\Html
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Builder
{
/**
* @var Collection
*/
public $collection;
/**
* @var Repository
*/
public $config;
/**
* @var Factory
*/
public $view;
/**
* @var HtmlBuilder
*/
public $html;
/**
* @var UrlGenerator
*/
public $url;
/**
* @var FormBuilder
*/
public $form;
/**
* @var string|array
*/
protected $ajax = '';
/**
* @var array
*/
protected $tableAttributes = ['class' => 'table', 'id' => 'dataTableBuilder'];
/**
* @var string
*/
protected $template = '';
/**
* @var array
*/
protected $attributes = [];
/**
* Lists of valid DataTables Callbacks.
*
* @link https://datatables.net/reference/option/.
* @var array
*/
protected $validCallbacks = [
'createdRow',
'drawCallback',
'footerCallback',
'formatNumber',
'headerCallback',
'infoCallback',
'initComplete',
'preDrawCallback',
'rowCallback',
'stateLoadCallback',
'stateLoaded',
'stateLoadParams',
'stateSaveCallback',
'stateSaveParams',
];
/**
* @param Repository $config
* @param Factory $view
* @param HtmlBuilder $html
* @param UrlGenerator $url
* @param FormBuilder $form
*/
public function __construct(
Repository $config,
Factory $view,
HtmlBuilder $html,
UrlGenerator $url,
FormBuilder $form
) {
$this->config = $config;
$this->view = $view;
$this->html = $html;
$this->url = $url;
$this->collection = new Collection;
$this->form = $form;
}
/**
* Generate DataTable javascript.
*
* @param null $script
* @param array $attributes
* @return string
*/
public function scripts($script = null, array $attributes = ['type' => 'text/javascript'])
{
$script = $script ?: $this->generateScripts();
return '<script' . $this->html->attributes($attributes) . '>' . $script . '</script>' . PHP_EOL;
}
/**
* Get generated raw scripts.
*
* @return string
*/
public function generateScripts()
{
$args = array_merge(
$this->attributes, [
'ajax' => $this->ajax,
'columns' => $this->collection->toArray(),
]
);
$parameters = $this->parameterize($args);
return sprintf(
$this->template(),
$this->tableAttributes['id'], $parameters
);
}
/**
* Generate DataTables js parameters.
*
* @param array $attributes
* @return string
*/
public function parameterize($attributes = [])
{
$parameters = (new Parameters($attributes))->toArray();
list($ajaxDataFunction, $parameters) = $this->encodeAjaxDataFunction($parameters);
list($columnFunctions, $parameters) = $this->encodeColumnFunctions($parameters);
list($callbackFunctions, $parameters) = $this->encodeCallbackFunctions($parameters);
$json = json_encode($parameters);
$json = $this->decodeAjaxDataFunction($ajaxDataFunction, $json);
$json = $this->decodeColumnFunctions($columnFunctions, $json);
$json = $this->decodeCallbackFunctions($callbackFunctions, $json);
return $json;
}
/**
* Encode ajax data function param.
*
* @param array $parameters
* @return mixed
*/
protected function encodeAjaxDataFunction($parameters)
{
$ajaxData = '';
if (isset($parameters['ajax']['data'])) {
$ajaxData = $parameters['ajax']['data'];
$parameters['ajax']['data'] = "#ajax_data#";
}
return [$ajaxData, $parameters];
}
/**
* Encode columns render function.
*
* @param array $parameters
* @return array
*/
protected function encodeColumnFunctions(array $parameters)
{
$columnFunctions = [];
foreach ($parameters['columns'] as $i => $column) {
unset($parameters['columns'][$i]['exportable']);
unset($parameters['columns'][$i]['printable']);
unset($parameters['columns'][$i]['footer']);
if (isset($column['render'])) {
$columnFunctions[$i] = $column['render'];
$parameters['columns'][$i]['render'] = "#column_function.{$i}#";
}
}
return [$columnFunctions, $parameters];
}
/**
* Encode DataTables callbacks function.
*
* @param array $parameters
* @return array
*/
protected function encodeCallbackFunctions(array $parameters)
{
$callbackFunctions = [];
foreach ($parameters as $key => $callback) {
if (in_array($key, $this->validCallbacks)) {
$callbackFunctions[$key] = $this->compileCallback($callback);
$parameters[$key] = "#callback_function.{$key}#";
}
}
return [$callbackFunctions, $parameters];
}
/**
* Compile DataTable callback value.
*
* @param mixed $callback
* @return mixed|string
*/
private function compileCallback($callback)
{
if (is_callable($callback)) {
return value($callback);
} elseif ($this->view->exists($callback)) {
return $this->view->make($callback)->render();
}
return $callback;
}
/**
* Decode ajax data method.
*
* @param string $function
* @param string $json
* @return string
*/
protected function decodeAjaxDataFunction($function, $json)
{
return str_replace("\"#ajax_data#\"", $function, $json);
}
/**
* Decode columns render functions.
*
* @param array $columnFunctions
* @param string $json
* @return string
*/
protected function decodeColumnFunctions(array $columnFunctions, $json)
{
foreach ($columnFunctions as $i => $function) {
$json = str_replace("\"#column_function.{$i}#\"", $function, $json);
}
return $json;
}
/**
* Decode DataTables callbacks function.
*
* @param array $callbackFunctions
* @param string $json
* @return string
*/
protected function decodeCallbackFunctions(array $callbackFunctions, $json)
{
foreach ($callbackFunctions as $i => $function) {
$json = str_replace("\"#callback_function.{$i}#\"", $function, $json);
}
return $json;
}
/**
* Get javascript template to use.
*
* @return string
*/
protected function template()
{
return $this->view->make(
$this->template ?: $this->config->get('datatables.script_template', 'datatables::script')
)->render();
}
/**
* Sets HTML table attribute(s).
*
* @param string|array $attribute
* @param mixed $value
* @return $this
*/
public function setTableAttribute($attribute, $value = null)
{
if (is_array($attribute)) {
$this->setTableAttributes($attribute);
} else {
$this->tableAttributes[$attribute] = $value;
}
return $this;
}
/**
* Sets multiple HTML table attributes at once.
*
* @param array $attributes
* @return $this
*/
public function setTableAttributes(array $attributes)
{
foreach ($attributes as $attribute => $value) {
$this->setTableAttribute($attribute, $value);
}
return $this;
}
/**
* Retrieves HTML table attribute value.
*
* @param string $attribute
* @return mixed
* @throws \Exception
*/
public function getTableAttribute($attribute)
{
if (! array_key_exists($attribute, $this->tableAttributes)) {
throw new \Exception("Table attribute '{$attribute}' does not exist.");
}
return $this->tableAttributes[$attribute];
}
/**
* Add a column in collection using attributes.
*
* @param array $attributes
* @return $this
*/
public function addColumn(array $attributes)
{
$this->collection->push(new Column($attributes));
return $this;
}
/**
* Add a Column object in collection.
*
* @param \Yajra\Datatables\Html\Column $column
* @return $this
*/
public function add(Column $column)
{
$this->collection->push($column);
return $this;
}
/**
* Set datatables columns from array definition.
*
* @param array $columns
* @return $this
*/
public function columns(array $columns)
{
foreach ($columns as $key => $value) {
if (! is_a($value, Column::class)) {
if (is_array($value)) {
$attributes = array_merge(['name' => $key, 'data' => $key], $this->setTitle($key, $value));
} else {
$attributes = [
'name' => $value,
'data' => $value,
'title' => $this->getQualifiedTitle($value),
];
}
$this->collection->push(new Column($attributes));
} else {
$this->collection->push($value);
}
}
return $this;
}
/**
* Set title attribute of an array if not set.
*
* @param string $title
* @param array $attributes
* @return array
*/
public function setTitle($title, array $attributes)
{
if (! isset($attributes['title'])) {
$attributes['title'] = $this->getQualifiedTitle($title);
}
return $attributes;
}
/**
* Convert string into a readable title.
*
* @param string $title
* @return string
*/
public function getQualifiedTitle($title)
{
return Str::title(str_replace(['.', '_'], ' ', Str::snake($title)));
}
/**
* Add a checkbox column.
*
* @param array $attributes
* @return $this
*/
public function addCheckbox(array $attributes = [])
{
$attributes = array_merge([
'defaultContent' => '<input type="checkbox" ' . $this->html->attributes($attributes) . '/>',
'title' => $this->form->checkbox('', '', false, ['id' => 'dataTablesCheckbox']),
'data' => 'checkbox',
'name' => 'checkbox',
'orderable' => false,
'searchable' => false,
'exportable' => false,
'printable' => true,
'width' => '10px',
], $attributes);
$this->collection->push(new Column($attributes));
return $this;
}
/**
* Add a action column.
*
* @param array $attributes
* @return $this
*/
public function addAction(array $attributes = [])
{
$attributes = array_merge([
'defaultContent' => '',
'data' => 'action',
'name' => 'action',
'title' => 'Action',
'render' => null,
'orderable' => false,
'searchable' => false,
'exportable' => false,
'printable' => true,
'footer' => '',
], $attributes);
$this->collection->push(new Column($attributes));
return $this;
}
/**
* Add a index column.
*
* @param array $attributes
* @return $this
*/
public function addIndex(array $attributes = [])
{
$indexColumn = Config::get('datatables.index_column', 'DT_Row_Index');
$attributes = array_merge([
'defaultContent' => '',
'data' => $indexColumn,
'name' => $indexColumn,
'title' => '',
'render' => null,
'orderable' => false,
'searchable' => false,
'exportable' => false,
'printable' => true,
'footer' => '',
], $attributes);
$this->collection->push(new Column($attributes));
return $this;
}
/**
* Setup ajax parameter
*
* @param string|array $attributes
* @return $this
*/
public function ajax($attributes)
{
$this->ajax = $attributes;
return $this;
}
/**
* Generate DataTable's table html.
*
* @param array $attributes
* @param bool $drawFooter
* @return string
*/
public function table(array $attributes = [], $drawFooter = false)
{
$this->tableAttributes = array_merge($this->tableAttributes, $attributes);
$th = $this->compileTableHeaders();
$htmlAttr = $this->html->attributes($this->tableAttributes);
$tableHtml = '<table ' . $htmlAttr . '>';
$tableHtml .= '<thead><tr>' . implode('', $th) . '</tr></thead>';
if ($drawFooter) {
$tf = $this->compileTableFooter();
$tableHtml .= '<tfoot><tr>' . implode('', $tf) . '</tr></tfoot>';
}
$tableHtml .= '</table>';
return $tableHtml;
}
/**
* Compile table headers and to support responsive extension.
*
* @return array
*/
private function compileTableHeaders()
{
$th = [];
foreach ($this->collection->toArray() as $row) {
$thAttr = $this->html->attributes(
array_only($row, ['class', 'id', 'width', 'style', 'data-class', 'data-hide'])
);
$th[] = '<th ' . $thAttr . '>' . $row['title'] . '</th>';
}
return $th;
}
/**
* Compile table footer contents.
*
* @return array
*/
private function compileTableFooter()
{
$footer = [];
foreach ($this->collection->toArray() as $row) {
$footer[] = '<th>' . $row['footer'] . '</th>';
}
return $footer;
}
/**
* Configure DataTable's parameters.
*
* @param array $attributes
* @return $this
*/
public function parameters(array $attributes = [])
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
/**
* Set custom javascript template.
*
* @param string $template
* @return $this
*/
public function setTemplate($template)
{
$this->template = $template;
return $this;
}
/**
* Get collection of columns.
*
* @return Collection
*/
public function getColumns()
{
return $this->collection;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Yajra\Datatables\Html;
use Illuminate\Support\Fluent;
/**
* Class Column.
*
* @package Yajra\Datatables\Html
* @see https://datatables.net/reference/option/ for possible columns option
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Column extends Fluent
{
/**
* @param array $attributes
*/
public function __construct($attributes = [])
{
$attributes['orderable'] = isset($attributes['orderable']) ? $attributes['orderable'] : true;
$attributes['searchable'] = isset($attributes['searchable']) ? $attributes['searchable'] : true;
$attributes['exportable'] = isset($attributes['exportable']) ? $attributes['exportable'] : true;
$attributes['printable'] = isset($attributes['printable']) ? $attributes['printable'] : true;
$attributes['footer'] = isset($attributes['footer']) ? $attributes['footer'] : '';
// Allow methods override attribute value
foreach ($attributes as $attribute => $value) {
$method = 'parse' . ucfirst(strtolower($attribute));
if (method_exists($this, $method)) {
$attributes[$attribute] = $this->$method($value);
}
}
parent::__construct($attributes);
}
/**
* Parse render attribute.
*
* @param mixed $value
* @return string|null
*/
public function parseRender($value)
{
/** @var \Illuminate\Contracts\View\Factory $view */
$view = app('view');
$parameters = [];
if (is_array($value)) {
$parameters = array_except($value, 0);
$value = $value[0];
}
if (is_callable($value)) {
return $value($parameters);
} elseif ($view->exists($value)) {
return $view->make($value)->with($parameters)->render();
}
return $value ? $this->parseRenderAsString($value) : null;
}
/**
* Display render value as is.
*
* @param mixed $value
* @return string
*/
private function parseRenderAsString($value)
{
return "function(data,type,full,meta){return $value;}";
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Yajra\Datatables\Html;
use Illuminate\Support\Fluent;
/**
* Class Parameters.
*
* @package Yajra\Datatables\Html
* @see https://datatables.net/reference/option/ for possible columns option
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Parameters extends Fluent
{
/**
* @var array
*/
protected $attributes = [
'serverSide' => true,
'processing' => true,
'ajax' => '',
'columns' => []
];
}

View File

@@ -0,0 +1,244 @@
<?php
namespace Yajra\Datatables\Processors;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Yajra\Datatables\Helper;
/**
* Class DataProcessor.
*
* @package Yajra\Datatables
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class DataProcessor
{
/**
* @var int
*/
protected $start;
/**
* Columns to escape value.
*
* @var array
*/
protected $escapeColumns = [];
/**
* Processed data output
*
* @var array
*/
protected $output = [];
/**
* @var array
*/
protected $appendColumns = [];
/**
* @var array
*/
protected $editColumns = [];
/**
* @var array
*/
protected $excessColumns = [];
/**
* @var mixed
*/
protected $results;
/**
* @var array
*/
protected $templates;
/**
* @var bool
*/
protected $includeIndex;
/**
* @param mixed $results
* @param array $columnDef
* @param array $templates
* @param int $start
*/
public function __construct($results, array $columnDef, array $templates, $start)
{
$this->results = $results;
$this->appendColumns = $columnDef['append'];
$this->editColumns = $columnDef['edit'];
$this->excessColumns = $columnDef['excess'];
$this->escapeColumns = $columnDef['escape'];
$this->includeIndex = $columnDef['index'];
$this->templates = $templates;
$this->start = $start;
}
/**
* Process data to output on browser
*
* @param bool $object
* @return array
*/
public function process($object = false)
{
$this->output = [];
$indexColumn = Config::get('datatables.index_column', 'DT_Row_Index');
foreach ($this->results as $row) {
$data = Helper::convertToArray($row);
$value = $this->addColumns($data, $row);
$value = $this->editColumns($value, $row);
$value = $this->setupRowVariables($value, $row);
$value = $this->removeExcessColumns($value);
if ($this->includeIndex) {
$value[$indexColumn] = ++$this->start;
}
$this->output[] = $object ? $value : $this->flatten($value);
}
return $this->escapeColumns($this->output);
}
/**
* Process add columns.
*
* @param mixed $data
* @param mixed $row
* @return array
*/
protected function addColumns($data, $row)
{
foreach ($this->appendColumns as $key => $value) {
$value['content'] = Helper::compileContent($value['content'], $data, $row);
$data = Helper::includeInArray($value, $data);
}
return $data;
}
/**
* Process edit columns.
*
* @param mixed $data
* @param mixed $row
* @return array
*/
protected function editColumns($data, $row)
{
foreach ($this->editColumns as $key => $value) {
$value['content'] = Helper::compileContent($value['content'], $data, $row);
Arr::set($data, $value['name'], $value['content']);
}
return $data;
}
/**
* Setup additional DT row variables.
*
* @param mixed $data
* @param mixed $row
* @return array
*/
protected function setupRowVariables($data, $row)
{
$processor = new RowProcessor($data, $row);
return $processor
->rowValue('DT_RowId', $this->templates['DT_RowId'])
->rowValue('DT_RowClass', $this->templates['DT_RowClass'])
->rowData('DT_RowData', $this->templates['DT_RowData'])
->rowData('DT_RowAttr', $this->templates['DT_RowAttr'])
->getData();
}
/**
* Remove declared hidden columns.
*
* @param array $data
* @return array
*/
protected function removeExcessColumns(array $data)
{
foreach ($this->excessColumns as $value) {
unset($data[$value]);
}
return $data;
}
/**
* Flatten array with exceptions.
*
* @param array $array
* @return array
*/
public function flatten(array $array)
{
$return = [];
$exceptions = ['DT_RowId', 'DT_RowClass', 'DT_RowData', 'DT_RowAttr'];
foreach ($array as $key => $value) {
if (in_array($key, $exceptions)) {
$return[$key] = $value;
} else {
$return[] = $value;
}
}
return $return;
}
/**
* Escape column values as declared.
*
* @param array $output
* @return array
*/
protected function escapeColumns(array $output)
{
return array_map(function ($row) {
if ($this->escapeColumns == '*') {
$row = $this->escapeRow($row, $this->escapeColumns);
} else {
foreach ($this->escapeColumns as $key) {
if (array_get($row, $key)) {
array_set($row, $key, e(array_get($row, $key)));
}
}
}
return $row;
}, $output);
}
/**
* Escape all values of row.
*
* @param array $row
* @param string|array $escapeColumns
* @return array
*/
protected function escapeRow(array $row, $escapeColumns)
{
foreach ($row as $key => $value) {
if (is_array($value)) {
$row[$key] = $this->escapeRow($value, $escapeColumns);
} else {
$row[$key] = e($value);
}
}
return $row;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Yajra\Datatables\Processors;
use Illuminate\Support\Arr;
use Yajra\Datatables\Helper;
/**
* Class RowProcessor.
*
* @package Yajra\Datatables
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class RowProcessor
{
/**
* @var mixed
*/
private $data;
/**
* @var mixed
*/
private $row;
/**
* @param mixed $data
* @param mixed $row
*/
public function __construct($data, $row)
{
$this->data = $data;
$this->row = $row;
}
/**
* Process DT RowId and Class value.
*
* @param string $attribute
* @param string|callable $template
* @return $this
*/
public function rowValue($attribute, $template)
{
if (! empty($template)) {
if (! is_callable($template) && Arr::get($this->data, $template)) {
$this->data[$attribute] = Arr::get($this->data, $template);
} else {
$this->data[$attribute] = Helper::compileContent($template, $this->data, $this->row);
}
}
return $this;
}
/**
* Process DT Row Data and Attr.
*
* @param string $attribute
* @param array $template
* @return $this
*/
public function rowData($attribute, array $template)
{
if (count($template)) {
$this->data[$attribute] = [];
foreach ($template as $key => $value) {
$this->data[$attribute][$key] = Helper::compileContent($value, $this->data, $this->row);
}
}
return $this;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace Yajra\Datatables;
use Exception;
use Illuminate\Http\Request as IlluminateRequest;
/**
* Class Request.
*
* @property array columns
* @package Yajra\Datatables
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class Request extends IlluminateRequest
{
/**
* Check if request uses legacy code
*
* @throws Exception
*/
public function checkLegacyCode()
{
if (! $this->get('draw') && $this->get('sEcho')) {
throw new Exception('DataTables legacy code is not supported! Please use DataTables 1.10++ coding convention.');
} elseif (! $this->get('draw') && ! $this->get('columns')) {
throw new Exception('Insufficient parameters');
}
}
/**
* Check if Datatables is searchable.
*
* @return bool
*/
public function isSearchable()
{
return $this->get('search')['value'] != '';
}
/**
* Get column's search value.
*
* @param integer $index
* @return string
*/
public function columnKeyword($index)
{
return $this->columns[$index]['search']['value'];
}
/**
* Check if Datatables must uses regular expressions
*
* @param integer $index
* @return string
*/
public function isRegex($index)
{
return $this->columns[$index]['search']['regex'] === 'true';
}
/**
* Get orderable columns
*
* @return array
*/
public function orderableColumns()
{
if (! $this->isOrderable()) {
return [];
}
$orderable = [];
for ($i = 0, $c = count($this->get('order')); $i < $c; $i++) {
$order_col = (int) $this->get('order')[$i]['column'];
$order_dir = $this->get('order')[$i]['dir'];
if ($this->isColumnOrderable($order_col)) {
$orderable[] = ['column' => $order_col, 'direction' => $order_dir];
}
}
return $orderable;
}
/**
* Check if Datatables ordering is enabled.
*
* @return bool
*/
public function isOrderable()
{
return $this->get('order') && count($this->get('order')) > 0;
}
/**
* Check if a column is orderable.
*
* @param integer $index
* @return bool
*/
public function isColumnOrderable($index)
{
return $this->get('columns')[$index]['orderable'] == 'true';
}
/**
* Get searchable column indexes
*
* @return array
*/
public function searchableColumnIndex()
{
$searchable = [];
for ($i = 0, $c = count($this->get('columns')); $i < $c; $i++) {
if ($this->isColumnSearchable($i, false)) {
$searchable[] = $i;
}
}
return $searchable;
}
/**
* Check if a column is searchable.
*
* @param integer $i
* @param bool $column_search
* @return bool
*/
public function isColumnSearchable($i, $column_search = true)
{
$columns = $this->get('columns');
if ($column_search) {
return $columns[$i]['searchable'] == 'true' && $columns[$i]['search']['value'] != '';
}
return $columns[$i]['searchable'] == 'true';
}
/**
* Get global search keyword
*
* @return string
*/
public function keyword()
{
return $this->get('search')['value'];
}
/**
* Get column identity from input or database.
*
* @param integer $i
* @return string
*/
public function columnName($i)
{
$column = $this->get('columns')[$i];
return isset($column['name']) && $column['name'] <> '' ? $column['name'] : $column['data'];
}
/**
* Check if Datatables allow pagination.
*
* @return bool
*/
public function isPaginationable()
{
return ! is_null($this->get('start')) && ! is_null($this->get('length')) && $this->get('length') != -1;
}
}

View File

@@ -0,0 +1,385 @@
<?php
namespace Yajra\Datatables\Services;
use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Facades\Config;
use Maatwebsite\Excel\Classes\LaravelExcelWorksheet;
use Maatwebsite\Excel\Writers\LaravelExcelWriter;
use Yajra\Datatables\Contracts\DataTableButtonsContract;
use Yajra\Datatables\Contracts\DataTableContract;
use Yajra\Datatables\Contracts\DataTableScopeContract;
use Yajra\Datatables\Datatables;
use Yajra\Datatables\Transformers\DataTransformer;
/**
* Class DataTable.
*
* @package Yajra\Datatables\Services
* @author Arjay Angeles <aqangeles@gmail.com>
*/
abstract class DataTable implements DataTableContract, DataTableButtonsContract
{
/**
* @var \Yajra\Datatables\Datatables
*/
protected $datatables;
/**
* @var \Illuminate\Contracts\View\Factory
*/
protected $viewFactory;
/**
* Datatables print preview view.
*
* @var string
*/
protected $printPreview = 'datatables::print';
/**
* List of columns to be exported.
*
* @var string|array
*/
protected $exportColumns = '*';
/**
* List of columns to be printed.
*
* @var string|array
*/
protected $printColumns = '*';
/**
* Query scopes.
*
* @var \Yajra\Datatables\Contracts\DataTableScopeContract[]
*/
protected $scopes = [];
/**
* Export filename.
*
* @var string
*/
protected $filename = '';
/**
* DataTable constructor.
*
* @param \Yajra\Datatables\Datatables $datatables
* @param \Illuminate\Contracts\View\Factory $viewFactory
*/
public function __construct(Datatables $datatables, Factory $viewFactory)
{
$this->datatables = $datatables;
$this->viewFactory = $viewFactory;
}
/**
* Process dataTables needed render output.
*
* @param string $view
* @param array $data
* @param array $mergeData
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
*/
public function render($view, $data = [], $mergeData = [])
{
if ($this->request()->ajax() && $this->request()->wantsJson()) {
return $this->ajax();
}
if ($action = $this->request()->get('action') AND in_array($action, ['print', 'csv', 'excel', 'pdf'])) {
if ($action == 'print') {
return $this->printPreview();
}
return call_user_func_array([$this, $action], []);
}
return $this->viewFactory->make($view, $data, $mergeData)->with('dataTable', $this->html());
}
/**
* Get Datatables Request instance.
*
* @return \Yajra\Datatables\Request
*/
public function request()
{
return $this->datatables->getRequest();
}
/**
* Display printable view of datatables.
*
* @return \Illuminate\Contracts\View\View
*/
public function printPreview()
{
$data = $this->getDataForPrint();
return $this->viewFactory->make($this->printPreview, compact('data'));
}
/**
* Get mapped columns versus final decorated output.
*
* @return array
*/
protected function getDataForPrint()
{
$columns = $this->printColumns();
return $this->mapResponseToColumns($columns, 'printable');
}
/**
* Get printable columns.
*
* @return array|string
*/
protected function printColumns()
{
return is_array($this->printColumns) ? $this->printColumns : $this->getColumnsFromBuilder();
}
/**
* Get columns definition from html builder.
*
* @return array
*/
protected function getColumnsFromBuilder()
{
return $this->html()->getColumns();
}
/**
* Optional method if you want to use html builder.
*
* @return \Yajra\Datatables\Html\Builder
*/
public function html()
{
return $this->builder();
}
/**
* Get Datatables Html Builder instance.
*
* @return \Yajra\Datatables\Html\Builder
*/
public function builder()
{
return $this->datatables->getHtmlBuilder();
}
/**
* Map ajax response to columns definition.
*
* @param mixed $columns
* @param string $type
* @return array
*/
protected function mapResponseToColumns($columns, $type)
{
return array_map(function ($row) use ($columns, $type) {
if ($columns) {
return (new DataTransformer())->transform($row, $columns, $type);
}
return $row;
}, $this->getAjaxResponseData());
}
/**
* Get decorated data as defined in datatables ajax response.
*
* @return array
*/
protected function getAjaxResponseData()
{
$this->datatables->getRequest()->merge(['length' => -1]);
$response = $this->ajax();
$data = $response->getData(true);
return $data['data'];
}
/**
* Export results to Excel file.
*
* @return void
*/
public function excel()
{
$this->buildExcelFile()->download('xls');
}
/**
* Build excel file and prepare for export.
*
* @return \Maatwebsite\Excel\Writers\LaravelExcelWriter
*/
protected function buildExcelFile()
{
/** @var \Maatwebsite\Excel\Excel $excel */
$excel = app('excel');
return $excel->create($this->getFilename(), function (LaravelExcelWriter $excel) {
$excel->sheet('exported-data', function (LaravelExcelWorksheet $sheet) {
$sheet->fromArray($this->getDataForExport());
});
});
}
/**
* Get export filename.
*
* @return string
*/
public function getFilename()
{
return $this->filename ?: $this->filename();
}
/**
* Set export filename.
*
* @param string $filename
* @return DataTable
*/
public function setFilename($filename)
{
$this->filename = $filename;
return $this;
}
/**
* Get filename for export.
*
* @return string
*/
protected function filename()
{
return 'export_' . time();
}
/**
* Get mapped columns versus final decorated output.
*
* @return array
*/
protected function getDataForExport()
{
$columns = $this->exportColumns();
return $this->mapResponseToColumns($columns, 'exportable');
}
/**
* Get export columns definition.
*
* @return array|string
*/
private function exportColumns()
{
return is_array($this->exportColumns) ? $this->exportColumns : $this->getColumnsFromBuilder();
}
/**
* Export results to CSV file.
*
* @return void
*/
public function csv()
{
$this->buildExcelFile()->download('csv');
}
/**
* Export results to PDF file.
*
* @return mixed
*/
public function pdf()
{
if ('snappy' == Config::get('datatables.pdf_generator', 'excel')) {
return $this->snappyPdf();
} else {
$this->buildExcelFile()->download('pdf');
}
}
/**
* PDF version of the table using print preview blade template.
*
* @return mixed
*/
public function snappyPdf()
{
$data = $this->getDataForPrint();
$snappy = app('snappy.pdf.wrapper');
$snappy->setOptions([
'no-outline' => true,
'margin-left' => '0',
'margin-right' => '0',
'margin-top' => '10mm',
'margin-bottom' => '10mm',
])->setOrientation('landscape');
return $snappy->loadView($this->printPreview, compact('data'))
->download($this->getFilename() . ".pdf");
}
/**
* Add basic array query scopes.
*
* @param \Yajra\Datatables\Contracts\DataTableScopeContract $scope
* @return $this
*/
public function addScope(DataTableScopeContract $scope)
{
$this->scopes[] = $scope;
return $this;
}
/**
* Apply query scopes.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $query
* @return mixed
*/
protected function applyScopes($query)
{
foreach ($this->scopes as $scope) {
$scope->apply($query);
}
return $query;
}
/**
* Get default builder parameters.
*
* @return array
*/
protected function getBuilderParameters()
{
return [
'order' => [[0, 'desc']],
'buttons' => [
'create',
'export',
'print',
'reset',
'reload',
],
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Yajra\Datatables\Transformers;
use Illuminate\Support\Collection;
/**
* Class DataTransformer.
*
* @package Yajra\Datatables\Transformers
* @author Arjay Angeles <aqangeles@gmail.com>
*/
class DataTransformer
{
/**
* Transform row data by columns definition.
*
* @param array $row
* @param mixed $columns
* @param string $type
* @return array
*/
public function transform(array $row, $columns, $type = 'printable')
{
if ($columns instanceof Collection) {
return $this->buildColumnByCollection($row, $columns, $type);
}
return array_only($row, $columns);
}
/**
* Transform row column by collection.
*
* @param array $row
* @param \Illuminate\Support\Collection $columns
* @param string $type
* @return array
*/
protected function buildColumnByCollection(array $row, Collection $columns, $type = 'printable')
{
$results = [];
foreach ($columns->all() as $column) {
if ($column[$type]) {
$title = $column['title'];
$data = array_get($row, $column['data']);
if ($type == 'exportable') {
$data = $this->decodeContent($data);
$title = $this->decodeContent($title);
}
$results[$title] = $data;
}
}
return $results;
}
/**
* Decode content to a readable text value.
*
* @param string $data
* @return string
*/
protected function decodeContent($data)
{
try {
$decoded = html_entity_decode(strip_tags($data), ENT_QUOTES, 'UTF-8');
return str_replace("\xc2\xa0", ' ', $decoded);
} catch (\Exception $e) {
return $data;
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
return [
/**
* DataTables search options.
*/
'search' => [
/**
* Smart search will enclose search keyword with wildcard string "%keyword%".
* SQL: column LIKE "%keyword%"
*/
'smart' => true,
/**
* Case insensitive will search the keyword in lower case format.
* SQL: LOWER(column) LIKE LOWER(keyword)
*/
'case_insensitive' => true,
/**
* Wild card will add "%" in between every characters of the keyword.
* SQL: column LIKE "%k%e%y%w%o%r%d%"
*/
'use_wildcards' => false,
],
/**
* DataTables fractal configurations.
*/
'fractal' => [
/**
* Request key name to parse includes on fractal.
*/
'includes' => 'include',
/**
* Default fractal serializer.
*/
'serializer' => 'League\Fractal\Serializer\DataArraySerializer',
],
/**
* DataTables script view template.
*/
'script_template' => 'datatables::script',
/**
* DataTables internal index id response column name.
*/
'index_column' => 'DT_Row_Index',
/**
* Namespaces used by the generator.
*/
'namespace' => [
/**
* Base namespace/directory to create the new file.
* This is appended on default Laravel namespace.
*
* Usage: php artisan datatables:make User
* Output: App\DataTables\UserDataTable
* With Model: App\User (default model)
* Export filename: users_timestamp
*/
'base' => 'DataTables',
/**
* Base namespace/directory where your model's are located.
* This is appended on default Laravel namespace.
*
* Usage: php artisan datatables:make Post --model
* Output: App\DataTables\PostDataTable
* With Model: App\Post
* Export filename: posts_timestamp
*/
'model' => '',
],
/**
* PDF generator to be used when converting the table to pdf.
* Available generators: excel, snappy
* Snappy package: barryvdh/laravel-snappy
* Excel package: maatwebsite/excel
*/
'pdf_generator' => 'excel',
];

View File

@@ -0,0 +1,27 @@
<?php
if (! function_exists('config_path')) {
/**
* Get the configuration path.
*
* @param string $path
* @return string
*/
function config_path($path = '')
{
return app()->basePath() . '/config' . ($path ? '/' . $path : $path);
}
}
if (! function_exists('public_path')) {
/**
* Return the path to public dir
*
* @param null $path
* @return string
*/
function public_path($path = null)
{
return rtrim(app()->basePath('public/' . $path), '/');
}
}

View File

@@ -0,0 +1,111 @@
(function ($, DataTable) {
"use strict";
var _buildUrl = function(dt, action) {
var url = dt.ajax.url() || '';
var params = dt.ajax.params();
params.action = action;
return url + '?' + $.param(params);
};
DataTable.ext.buttons.excel = {
className: 'buttons-excel',
text: function (dt) {
return '<i class="fa fa-file-excel-o"></i> ' + dt.i18n('buttons.excel', 'Excel');
},
action: function (e, dt, button, config) {
var url = _buildUrl(dt, 'excel');
window.location = url;
}
};
DataTable.ext.buttons.export = {
extend: 'collection',
className: 'buttons-export',
text: function (dt) {
return '<i class="fa fa-download"></i> ' + dt.i18n('buttons.export', 'Export') + '&nbsp;<span class="caret"/>';
},
buttons: ['csv', 'excel', 'pdf']
};
DataTable.ext.buttons.csv = {
className: 'buttons-csv',
text: function (dt) {
return '<i class="fa fa-file-excel-o"></i> ' + dt.i18n('buttons.csv', 'CSV');
},
action: function (e, dt, button, config) {
var url = _buildUrl(dt, 'csv');
window.location = url;
}
};
DataTable.ext.buttons.pdf = {
className: 'buttons-pdf',
text: function (dt) {
return '<i class="fa fa-file-pdf-o"></i> ' + dt.i18n('buttons.pdf', 'PDF');
},
action: function (e, dt, button, config) {
var url = _buildUrl(dt, 'pdf');
window.location = url;
}
};
DataTable.ext.buttons.print = {
className: 'buttons-print',
text: function (dt) {
return '<i class="fa fa-print"></i> ' + dt.i18n('buttons.print', 'Print');
},
action: function (e, dt, button, config) {
var url = _buildUrl(dt, 'print');
window.location = url;
}
};
DataTable.ext.buttons.reset = {
className: 'buttons-reset',
text: function (dt) {
return '<i class="fa fa-undo"></i> ' + dt.i18n('buttons.reset', 'Reset');
},
action: function (e, dt, button, config) {
dt.search('').draw();
}
};
DataTable.ext.buttons.reload = {
className: 'buttons-reload',
text: function (dt) {
return '<i class="fa fa-refresh"></i> ' + dt.i18n('buttons.reload', 'Reload');
},
action: function (e, dt, button, config) {
dt.draw(false);
}
};
DataTable.ext.buttons.create = {
className: 'buttons-create',
text: function (dt) {
return '<i class="fa fa-plus"></i> ' + dt.i18n('buttons.create', 'Create');
},
action: function (e, dt, button, config) {
window.location = window.location.href.replace(/\/+$/, "") + '/create';
}
};
})(jQuery, jQuery.fn.dataTable);

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Print Table</title>
<meta charset="UTF-8">
<meta name=description content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<style>
body {margin: 20px}
</style>
</head>
<body>
<table class="table table-bordered table-condensed table-striped">
@foreach($data as $row)
@if ($row == reset($data))
<tr>
@foreach($row as $key => $value)
<th>{!! $key !!}</th>
@endforeach
</tr>
@endif
<tr>
@foreach($row as $key => $value)
@if(is_string($value) || is_numeric($value))
<td>{!! $value !!}</td>
@else
<td></td>
@endif
@endforeach
</tr>
@endforeach
</table>
</body>
</html>

View File

@@ -0,0 +1 @@
(function(window,$){window.LaravelDataTables=window.LaravelDataTables||{};window.LaravelDataTables["%1$s"]=$("#%1$s").DataTable(%2$s);})(window,jQuery);