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);
}
Related
I have a table that contains products, and I want to automate pricing.
I've tried to create a table called price.
The price table is structured like so :
max_height
max_width
max_long
max_weight
min_height
min_width
min_long
min_weight
price
I want to retreive the price depending on the ( height - width - long - weight ) of product
I've tried this way :
From the controller :
<?php
namespace App\Http\Controllers;
use App\Models\Coli;
use App\Models\Pricing;
use Illuminate\Pipeline\Pipeline;
class ColiPriceController extends Controller
{
public static $data = [];
public static function price($id){
ColiPriceController::setData($id);
$price = app(Pipeline::class)
->send(Pricing::query())
->through([
\App\Filters\Pricing\MaxHeightPriceFilter::class,
\App\Filters\Pricing\MinHeightPriceFilter::class,
\App\Filters\Pricing\MaxLongPriceFilter::class,
\App\Filters\Pricing\MinLongPriceFilter::class,
\App\Filters\Pricing\MaxWidthPriceFilter::class,
\App\Filters\Pricing\MinwidthPriceFilter::class,
\App\Filters\Pricing\MaxWeightPriceFilter::class,
\App\Filters\Pricing\MinWeightPriceFilter::class,
])
->thenReturn()
->first();
}
protected static function setData($id)
{
$coli = Coli::find($id);
$coli->height = ($coli->height) ? intval($coli->height) : 0;
$coli->width = ($coli->width) ? intval($coli->width) : 0;
$coli->longeur = ($coli->longeur) ? intval($coli->longeur) : 0;
$coli->wieght = ($coli->wieght) ? intval($coli->wieght) : 0;
$data = [
'height' => $coli->height,
'width' => $coli->width,
'long' => $coli->longeur,
'weight' => $coli->wieght,
];
return ColiPriceController::$data = $data;
}
}
From MaxHeightFilter :
<?php
namespace App\Filters\Pricing;
use Closure;
class MaxHeightPriceFilter extends PriceFilter
{
public $column = "max_height";
public $dataColumn = "height";
public $operator = "<=";
public function handle($request, Closure $next)
{
return $this->filter($request, $next);
}
}
From PriceFilter :
<?php
namespace App\Filters\Pricing;
use Illuminate\Database\Eloquent\Builder;
use App\Http\Controllers\ColiPriceController;
class PriceFilter
{
public $column = "max_weight";
public $dataColumn = "weight";
public $operator = "<=";
protected function filter($request, $next)
{
if($this->chequePriceToContinue($request)){
return $next(static::removeWhere($request, $this->column));
}
return $next($request);
// return $next($request->where($this->column, $this->operator, ':'.ColiPriceController::$data[$this->dataColumn]));
}
public function chequePriceToContinue($request){
$price = $request->where($this->column, $this->operator, ColiPriceController::$data[$this->dataColumn] )->get();
if(is_array($price)){
return true;
}
return false;
}
/**
* #param Builder $builder
* #param $whereColumn
* #return Builder
*/
public static function removeWhere(Builder $builder, $whereColumn)
{
$bindings = $builder->getQuery()->bindings['where'];
$wheres = $builder->getQuery()->wheres;
$whereKey = false;
foreach ($wheres as $key => $where) {
if ($where['column'] == $whereColumn) {
$whereKey = $key;
break;
}
}
if ($whereKey !== false) {
unset($bindings[$whereKey]);
unset($wheres[$whereKey]);
}
$builder->getQuery()->wheres = $wheres;
$builder->getQuery()->bindings['where'] = $bindings;
return $builder;
}
}
Ok, based on your info, I think your solution should look something like this:
First, make sure you have a Price model if you don't have it already.
Second, build your query in your controller with your model like so:
//ColiPriceController.php
function findPrices(Request $request) {
//If id is provided, take coli from db
if($request->has('id')) {
//FindOrFail returns 404 if not found
$coli = Coli::findOrFail($request->id);
}
//Start query.
$query = Price::query();
//Set values based on coli if set OR by request parameters
$height = $coli->height ?? $request->height;
$width = $coli->width ?? $request->width;
$long = $coli->long ?? $request->long;
$weight = $coli->weight ?? $request->weight;
//If the initialized height/width/long/weight is greater than 0, add the where clauses to query
if($height > 0) {
$query
->where('max_height', '>', $height)
->where('min_height', '<', $height);
}
if($width > 0) {
$query
->where('max_width', '>', $width)
->where('min_width', '<', $width);
}
if($long > 0) {
$query
->where('max_long', '>', $long)
->where('min_long', '<', $long);
}
if($weight > 0) {
$query
->where('max_weight', '>', $weight)
->where('min_weight', '<', $weight);
}
//I'm using get() and not first() because im not sure if you can guarantee you only find 1 result and not multiple.
//If you are confident you only get 1 result per query, feel free to make it first()
return $query->get();
}
So here I only add the where clauses if you initialized the respective variable. This is not mandatory but seemed logical from my point of view. The findPrices function should be called by a route. You can pass id, height, width,long, weight as request parameters. If id is set and the coli is found, the other parameters are ignored.
I hope it helps, any feedback is welcome
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 table that I want to use to show records of timesheet logs, I've been able to do a filter using whereHas which works, but when I try to filter by employee I still get the logs for all employees attribtued to those jobs instead of the one I'm searching for.
My controller:
$request = json_decode(json_encode($request->all(), true))->params;
$jobs = Job::whereHas('timesheets', function($query) use ($request) {
if (count($request->selected_employees) > 0) {
$query->wherein('employee_id', $request->selected_employees);
}
if (count($request->selected_clients) > 0) {
$query->wherein('client_id', $request->selected_clients);
}
if (!empty($request->start_date)) {
$query->where('date','>=',$request->start_date);
}
if (!empty($request->end_date)) {
$query->where('date','<=',$request->end_date);
}
});
$jobs = (new Job)->generateReport($jobs->get(), $request->selected_employees);
$result = array_merge_recursive($jobs);
return $result;
My Model which iterates through the job. So far everything is accurate except for the child relationship called 'timesheets', it's not defined here, but laravel auto populates it and I am not able to overwrite/replace anything with that attribute. Any ideas?
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Job extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ["client_id", "job_number", "job_date", "payment_type", "rate", "description"];
public $totalHoursArray = array();
/**
* #var array|mixed
*/
private $report_totals;
public function client() {
return $this->belongsTo(Client::class);
}
public function timesheets() {
return $this->hasMany(TimesheetLog::class);
}
public function creator(){
return $this->belongsTo(User::class,'created_by');
}
public function editor(){
return $this->belongsTo(User::class,'edited_by');
}
/**
*
* Returns a count of Employee Hours per job for timesheet entries
* currently selected in the Job model
*
* #return array
*/
public function getEmployeeHoursPerJob($employee_ids){
$i = 0;
$hours_per_job = array();
$timesheets = empty($employee_ids) ? $this->timesheets : $this->timesheets->whereIn('employee_id',$employee_ids);
foreach ( $timesheets as $trow) {
$trow->employee_code = Employee::find($trow->employee_id)->code;
$date = new \DateTime($trow->date);
$trow->date = date_format($date, 'd-m-Y');
//find if the employee exists in the hours per job array if not, push a new row
$found = array_search($trow->employee_id,array_column($hours_per_job, 'employee_id', isset($hours_per_job['employee_id']) ? 'employee_id' : null));
if($i > 0 && $found !== false){
$hours_per_job[$found]['total_time'] += $trow->total_time;
} else {
array_push($hours_per_job, ['employee_id' => $trow->employee_id, 'employee_code' => $trow->employee_code, 'total_time' => ($trow->total_time)]);
}
$i++;
}
return $hours_per_job;
}
public function generateReport($jobs, Array $employee_ids){
$report_totals = array();
$filtered_timesheets = array();
foreach ($jobs AS $jobrow) {
$i = 0;
$jobrow->client_name = Client::find($jobrow->client_id)->name;
$jobrow->attention = Client::find($jobrow->client_id)->attention;
$jobrow->rate = "$".$jobrow->rate ." ". $jobrow->payment_type;
$dateT = new \DateTime($jobrow->job_date);
$jobrow->job_date = date_format($dateT, 'd-m-Y');
$hours = $jobrow->getEmployeeHoursPerJob($employee_ids);
$jobrow->employee_hours = $hours;
foreach ($filtered_timesheets as $timesheetf){
array_push($timesheets_filtered, $timesheetf);
}
foreach($hours AS $hoursRow){
$found = array_search($hoursRow['employee_id'],array_column($report_totals, 'employee_id',
isset($report_totals['employee_id']) ? 'employee_id' : null));
if($found !== false){
$report_totals[$found]['total_time'] += $hoursRow['total_time'];
} else {
array_push($report_totals, $hoursRow);
$i++;
}
}
}
return compact('jobs','report_totals');
}
}
In the foreach loop I assigned a new property of the row to a wherein query and this was accurate and what I wanted. But again, I couldn't replace or assign the original property that I want to send to the view.
$jobrow->timesheets_filtered = $jobrow->timesheets->wherein('employee_id',$employee_ids)->toArray();
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
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!.