I would like to create a more readable code by eliminating too many if statements but still does the job. I have tried creating a private method and extract the date range query and return the builder instance but whenever I do that, it does not return the correct builder query result so I end up smashing everything up on this method.
Other parameters will be added soon, so the if statements would pill up very fast. :(
Any tip on how to improve would be much appreciated. Thanks!
/**
* #param array $params
*
* #param $orderBy
* #param $sortBy
*
* #return Collection
*
* Sample:
* `/orders?release_date_start=2018-01-01&release_date_end=2018-02-20&firm_id=3` OR
* `/orders?claimId=3&status=completed`
*
* Problem: Too many if statements
*
*/
public function findOrdersBy(array $params, $orderBy = 'id', $sortBy = 'asc'): Collection
{
$release_date_start = array_get($params, 'release_date_start');
$release_date_end = array_get($params, 'release_date_end');
$claimId = array_get($params, 'claimId');
$firm_id = array_get($params, 'firm_id');
$status = array_get($params, 'status');
$orders = $this->model->newQuery();
if (!is_null($release_date_start) && !is_null($release_date_end)) {
$orders->whereBetween('releaseDate', [$release_date_start, $release_date_end]);
} else {
if (!is_null($release_date_start)) {
$orders->where('releaseDate', '>=', $release_date_start);
} else {
if (!is_null($release_date_end)) {
$orders->where('releaseDate', '<=', $release_date_end);
}
}
}
if (!is_null($claimId)) {
$orders->where(compact('claimId'));
}
if (!is_null($firm_id)) {
$orders->orWhere(compact('firm_id'));
}
if (!is_null($status)) {
$orders->where(compact('status'));
}
return $orders->orderBy($orderBy, $sortBy)->get();
}
if you are interested in using collection methods then you can use when() collection method to omit your if-else statements. So according to your statement it will look something like:
$orders->when(!is_null($release_date_start) && !is_null($release_date_end), function($q) {
$q->whereBetween('releaseDate', [$release_date_start, $release_date_end]);
}, function($q) {
$q->when(!is_null($release_date_start), function($q) {
$q->where('releaseDate', '>=', $release_date_start);
}, function($q) {
$q->when(!is_null($release_date_end), function($q) {
$q->where('releaseDate', '<=', $release_date_end);
})
})
})
->when(!is_null($claimId), function($q) {
$q->where(compact('claimId'));
})
->when(!is_null($firm_id), function($q) {
$q->orWhere(compact('firm_id'));
})
->when(!is_null($status), function($q) {
$q->where(compact('status'));
})
For more information you can see conditional-clauses in documentation. Hope this helps.
One option you can use is ternary operation in php like this:
$claimId ? $orders->where(compact('claimId')) : ;
$firm_id ? $orders->orWhere(compact('firm_id')) : ;
$status ? $orders->where(compact('status')) : ;
It would be cleaner than is statements code.
Another option you can use in laravel is Conditional Clauses
Thanks for your suggestions but I came up with another solution:
/**
* #param array $params
*
* #param $orderBy
* #param $sortBy
*
* #return Collection
*/
public function findOrdersBy(array $params, $orderBy = 'id', $sortBy = 'asc'): Collection
{
$release_date_start = array_get($params, 'release_date_start');
$release_date_end = array_get($params, 'release_date_end');
$orders = $this->model->newQuery();
if (!is_null($release_date_start) && !is_null($release_date_end)) {
$orders->whereBetween('releaseDate', [$release_date_start, $release_date_end]);
} else {
if (!is_null($release_date_start)) {
$orders->where('releaseDate', '>=', $release_date_start);
} else {
if (!is_null($release_date_end)) {
$orders->where('releaseDate', '<=', $release_date_end);
}
}
}
$fields = collect($params)->except($this->filtersArray())->all();
$orders = $this->includeQuery($orders, $fields);
return $orders->orderBy($orderBy, $sortBy)->get();
}
/**
* #param Builder $orderBuilder
* #param array $params
*
* #return Builder
*/
private function includeQuery(Builder $orderBuilder, ... $params) : Builder
{
$orders = [];
foreach ($params as $param) {
$orders = $orderBuilder->where($param);
}
return $orders;
}
/**
* #return array
*/
private function filtersArray() : array
{
return [
'release_date_start',
'release_date_end',
'order_by',
'sort_by',
'includes'
];
}
The main factor on the private method includeQuery(Builder $orderBuilder, ... $params) which takes $params as variable length argument. We just iterate the variables being passed as a query parameter /orders?code=123&something=test and pass those as a where() clause in the query builder.
Some parameters may not be a property of your object so we have to filter only the query params that match the object properties. So I created a filtersArray() that would return the parameters to be excluded and prevent an error.
Hmmm, actually, while writing this, I should have the opposite which is only() otherwise it will have an infinite of things to exclude. :) That would be another refactor I guess. :P
Related
I'm trying to make reusable datatable instance
My Datatable Class :
class Datatables extends CI_Model {
protected $columnOrder;
protected $columnSearch;
protected $query;
public function __construct($columnOrder,$columnSearch,$query)
{
parent::__construct();
$this->columnOrder = $columnOrder;
$this->columnSearch = $columnSearch;
$this->query = $query;
}
/**
* Generate db query
*
* #return object
*/
private function getDatatablesQuery()
{
$i = 0;
foreach ($this->columnSearch as $item) {
if(#$_POST['search']['value']) {
if($i===0) {
$this->query->group_start();
$this->query->like($item, $_POST['search']['value']);
} else {
$this->query->or_like($item, $_POST['search']['value']);
}
if(count($this->columnSearch) - 1 == $i)
$this->query->group_end();
}
$i++;
}
if(isset($_POST['order'])) {
$this->query->order_by($this->columnOrder[$_POST['order']['0']['column']], $_POST['order']['0']['dir']);
} else if(isset($this->order)) {
$order = $this->order;
$$this->query->order_by(key($order), $order[key($order)]);
}
}
/**
* Generate db result
*
* #return integer
*/
public function getDatatables()
{
$this->getDatatablesQuery();
if(#$_POST['length'] != -1) $this->query->limit(#$_POST['length'], #$_POST['start']);
$query = $this->query->get();
return $query->result();
}
/**
* Count filtered rows
*
* #return integer
*/
public function countFiltered()
{
$query = $this->query->get();
return $query->num_rows;
}
/**
* Count all rows
*
* #return integer
*/
public function countAll()
{
return $this->query->count_all_results();
}
}
My FmrTable Class
<?php defined('BASEPATH') OR exit('No direct script access alowed');
require 'application/libraries/Datatables/Datatables.php';
class FmrTable {
protected $select;
protected $columnOrder;
protected $columnSearch;
protected $ci;
public function __construct()
{
$this->select = 'fmrs.id as id,sections.name as section,users.username as user,fmr_no,fmrs.status';
$this->columnOrder = ['id','section','user','fmr_no','status'];
$this->columnSearch = ['section','user','fmr_no','status'];
$this->ci = get_instance();
}
public function get()
{
$query = $this->ci->db
->select($this->select)
->from('fmrs')
->join('sections as sections', 'fmrs.section_id = sections.id', 'LEFT')
->join('users as users', 'fmrs.user_id = users.id', 'LEFT');
$query->where('section_id',$this->ci->session->userdata('section-fmr'));
}
$datatable = new Datatables($this->columnOrder,$this->columnSearch,$query);
return [
'list' => $datatable->getDatatables(),
'countAll' => $datatable->countAll(),
'countFiltered' => $datatable->countFiltered()
];
}
}
This always throw a database error that says Error Number: 1096 No tables used
This came from the countFiltered() method, when i tried to dump the $query without get(), it returned the correct object instance but if i do this then the num_rows property will never available, but when i add the get() method, it will return the 1096 error number
How to solve this ?
A call to ->get() resets the query builder. So when you call ->get() for the second time (in countFiltered) the table name and the remainder of the query have been cleared and that's why you get the error.
Solution is to use query builder caching. This allows you to cache part of the query (between start_cache and stop_cache) and execute it multiple times: https://www.codeigniter.com/userguide3/database/query_builder.html?highlight=start_cache#query-builder-caching
Use flush_cache to clear the cache afterwards, so the cached query part does not interfere with subsequent queries in the same request:
FmrTable
public function get()
{
$this->ci->db->start_cache();
$query = $this->ci->db
->select($this->select)
->from('fmrs')
->join('sections as sections', 'fmrs.section_id = sections.id', 'LEFT')
->join('users as users', 'fmrs.user_id = users.id', 'LEFT');
$query->where('section_id',$this->ci->session->userdata('section-fmr'));
//}
$this->ci->db->stop_cache();
$datatable = new Datatables($this->columnOrder,$this->columnSearch,$query);
$result = [
'list' => $datatable->getDatatables(),
'countAll' => $datatable->countAll(),
'countFiltered' => $datatable->countFiltered()
];
$this->ci->db->flush_cache();
return $result;
}
And probably use num_rows() instead of num_rows here, num_rows gave me a NULL instead of a count:
Datatables
/**
* Count filtered rows
*
* #return integer
*/
public function countFiltered()
{
$query = $this->query->get();
return $query->num_rows();
}
I have a find() method in my if else statement that queries the database and returns the data as an array. The if part works fine. The problem is in the else part. When I try to access the index interface in the browser, am getting this error.
Unable to locate an object compatible with paginate.
RuntimeException
From what I have gathered so far, the paginate() method works with objects not arrays. Am stuck on how to come to my desired outcome. Am new to CakePHP, a not so advanced/complicated response would be appreciated. Thanks
/**
* Assets Controller
*
*
* #method \App\Model\Entity\Asset[] paginate($object = null, array $settings = [])
*/
class AssetsController extends AppController
{
/**
* Index method
*
* #return \Cake\Http\Response|void
*/
public function index()
{
$this->loadModel('Users');
$username = $this->request->session()->read('Auth.User.username');
$userdetail = $this->Users->find('all')->where(['username' => $username])->first();
$school = $userdetail->school_unit;
$roleid = $userdetail->role_id;
if ($roleid == 1) {
$this->paginate = [
'contain' => ['SchoolUnits', 'AssetConditions', 'AssetCategories', 'AssetGroups', 'AssetStatus']
];
$assets = $this->paginate($this->Assets);
$this->set(compact('assets'));
$this->set('_serialize', ['assets']);
} else {
$results = $this->Assets->find('all')->contain(['SchoolUnits', 'AssetConditions', 'AssetCategories', 'AssetGroups', 'AssetStatus'])->where(['school_unit_id' => $school])->first();
$assets = $this->paginate($this->$results);
$this->set(compact('assets'));
$this->set('_serialize', ['assets']);
}
}
Maybe I don't understand this idea but I'm starting create my first test app that use api platform.
I have custom action for my search:
/**
* #Route(
* path="/api/pharmacies/search",
* name="pharmacy_search",
* defaults={
* "_api_resource_class"=Pharmacy::class,
* "_api_collection_operation_name"="search"
* }
* )
* #param Request $request
*
* #return Paginator
*/
public function search(Request $request)
{
$query = SearchQuery::createFromRequest($request);
return $this->pharmacyRepository->search($query);
}
and method in the repository:
public function search(SearchQuery $searchQuery: Paginator
{
$firstResult = ($searchQuery->getPage() - 1) * self::ITEMS_PER_PAGE;
$qb = $this->createQueryBuilder('p')
/...
->setFirstResult($firstResult)
->setMaxResults(self::ITEMS_PER_PAGE);
$doctrinePaginator = new DoctrinePaginator($qb, false);
return new Paginator($doctrinePaginator);
}
And it works fine but this action doesn't need all field form the entity and relations to other tables. Currently this action creates 22 queries. I'd like create query in the DBAL/QueryBuilder and return pagination object with DTO.
public function search(SearchQuery $searchQuery)
{
.../
$qb = $this->createQueryBuilder('p')
->select('p.id')
->addSelect('p.name')/....
$rows = $qb->getQuery()->getArrayResult();
foreach ($rows as $row){
$data[] = new SearchPharmacy($row['id'], $row['name']);
}
return $data;
}
The above code will work but if the response isn't Pagination object and I don't have hydra:totalItems, hydra:next etc in the response.
In theory I can use DataTransformer and transform entity to DTO, but this way can't allow simplify database queries.
Can I achieve this?
I don't know how to use dbal query and mapped the result on a DTO, but I know how to add custom select, return it like a scalar:
public function search(SearchQuery $searchQuery): Paginator
{
$firstResult = ($searchQuery->getPage() - 1) * self::ITEMS_PER_PAGE;
$qb = $this->createQueryBuilder('p')
->select('p.id')
->addSelect('p.name')
->where('p.name LIKE :search')
->setParameter('search', "%" . $searchQuery->getSearch() . "%");
$query = $qb->getQuery()->setFirstResult($firstResult)
->setMaxResults(self::ITEMS_PER_PAGE);
$doctrinePaginator = new DoctrinePaginator($query, false);
$doctrinePaginator->setUseOutputWalkers(false);
return new Paginator($doctrinePaginator);
}
It finally I have 2 light queries, selected field from entity and Pagination.
Sources:
https://github.com/APY/APYDataGridBundle/issues/931
https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/cookbook/dql-custom-walkers.html
I have a search form where I take several parameters and I return the results narrowed down. However, I don't get how I can chain the requests properly.
Like, I can't put Candidate::all() to have all the values and narrow them down since it's a collection. How can I make sure that my request will follow from the past request?
Here's is my request (only the first parameter).
So, how can I chain them properly?
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$candidates = Candidate::all();
$data = [];
$data['availabilities'] = Availability::all();
$data['job_types'] = JobType::all();
$data['fields_of_work'] = FieldOfWork::all();
$data['interests'] = Interest::all();
$data['salary'] = Salary::all();
$data['trainings'] = Training::all();
if($request->availaibilities && $request->availabilities !== -1) {
$candidates = Candidate::whereHas('availabilities', function ($query) use ($request) {
$query->where('availabilities.id', $request->field);
});
}
return view('admin.candidates.index')->with('candidates', $candidates->get())->with('data', $data)->with('request', $request);
}
You can add conditions on the query builder instance from Candidate::query()
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$candidates = Candidate::query();
$data = [];
$data['availabilities'] = Availability::all();
$data['job_types'] = JobType::all();
$data['fields_of_work'] = FieldOfWork::all();
$data['interests'] = Interest::all();
$data['salary'] = Salary::all();
$data['trainings'] = Training::all();
if ($request->availaibilities && $request->availabilities !== -1 && $request->field) {
$candidates->whereHas('availabilities', function ($query) use ($request) {
$query->where('availabilities.id', $request->field);
});
}
if ($request->secondParameter) {
$candidates->where('secondParameter', $request->secondParameter);
}
// and so on
return view('admin.candidates.index')->with('candidates', $candidates->get())->with('data', $data)->with('request', $request);
}
I need to check that {subcategory} has parent category {category}. How i can get the model of {category} in second binding?
I tried $route->getParameter('category');. Laravel throws FatalErrorException with message "Maximum function nesting level of '100' reached, aborting!".
Route::bind('category', function ($value) {
$category = Category::where('title', $value)->first();
if (!$category || $category->parent_id) {
App::abort(404);
}
return $category;
});
Route::bind('subcategory', function ($value, $route) {
if ($value) {
$category = Category::where('title', $value)->first();
if ($category) {
return $category;
}
App::abort(404);
}
});
Route::get('{category}/{subcategory?}', 'CategoriesController#get');
Update:
Now i made this, but i think it's not the best solution.
Route::bind('category', function ($value) {
$category = Category::where('title', $value)->whereNull('parent_id')->first();
if (!$category) {
App::abort(404);
}
Route::bind('subcategory', function ($value, $route) use ($category) {
if ($value) {
$subcategory = Category::where('title', $value)->where('parent_id', $category->id)->first();
if (!$subcategory) {
App::abort(404);
}
return $subcategory;
}
});
return $category;
});
You may try this and should work, code is self explanatory (ask if need an explanation):
Route::bind('category', function ($value) {
$category = Category::where('title', $value)->first();
if (!$category || $category->parent_id) App::abort(404);
return $category;
});
Route::bind('subcategory', function ($value, $route) {
$category = $route->parameter('category');
$subcategory = Category::where('title', $value)->whereParentId($category->id);
return $subcategory->first() ?: App::abort(404); // shortcut of if
});
Route::get('{category}/{subcategory?}', 'CategoriesController#get');
Update: (As OP claimed that, there is no parameter method available in Route class):
/**
* Get a given parameter from the route.
*
* #param string $name
* #param mixed $default
* #return string
*/
public function getParameter($name, $default = null)
{
return $this->parameter($name, $default);
}
/**
* Get a given parameter from the route.
*
* #param string $name
* #param mixed $default
* #return string
*/
public function parameter($name, $default = null)
{
return array_get($this->parameters(), $name) ?: $default;
}
I can't test this right now for you, but the closure function receives a $value and $route.
The last one is a instance of '\Illuminate\Routing\Route' (http://laravel.com/api/class-Illuminate.Routing.Route.html), and perhaps you could use the getParameter() method to retrieve some data....
I recently ran into same issues while trying to auto validate my stories existence inside my Session Model. So, i tried to check my Story model existence inside my Session Model using model bindings.
This is my solution
$router->bind('stories', function($value, $route) {
$routeParameters = $route->parameters();
$story = Story::where('id', $value)->where('poker_session_id', $routeParameters['poker_sessions']->id)->first();
if (!$story) {
throw new NotFoundHttpException;
}
return $story;
});
You can actually get route parameters using $route->parameters(), which returns an array. In my case, "poker_sessions" key contain an PokerSession Model, which i want.
Please be careful and use this only when you have a /category/{category}/subcategory/{subcategory} url like. Not subcategory without any {category}.
Good luck!.