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();
}
Related
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']);
}
}
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 am am trying to save to my database, and as part of that save I am trying to sync my many to many relationship, however I am getting the following error from my API,
"BadMethodCallException","message":"Call to undefined method Illuminate\\Database\\Query\\Builder::sync()"
I would have thought that this is because the relationships I have in my model are not many to many so cant be synced, but they look correct to me,
class Organisation extends Eloquent {
//Organsiation __has_many__ users (members)
public function users()
{
return $this->belongsToMany('User')->withPivot('is_admin');
}
//Organisation __has_many__ clients
public function clients()
{
return $this->belongsToMany('Client');
}
//Organisation __has_many__ teams
public function teams()
{
return $this->belongsToMany('Team');
}
//Organisation __has_many__ projects
public function projects()
{
return $this->hasMany('Project');
}
}
class User extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = array('password', 'remember_token');
public function organisations()
{
return $this->belongsToMany('Organisation')->withPivot('is_admin');
}
}
I am running the sync after a successful save,
if(isset($members)) {
$organisation->users()->sync($members);
}
and members is certainly set. The organsisation is created in the following way,
public function create()
{
//
$postData = Input::all();
$rules = array(
'name' => 'required',
);
$validation = Validator::make(Input::all(), $rules);
if($validation->fails()) {
return Response::json( $validation->messages()->first(), 500);
} else {
$organisation = new Organisation;
// Save the basic organistion data.
$organisation->name = $postData['name'];
$organisation->information = $postData['information'];
$organisation->type = 'organisation';
/*
* Create an array of users that can used for syncinng the many-to-many relationship
* Loop the array to assign admins to the organisation also.
*/
if(isset($postData['members'])) {
$members = array();
foreach($postData['members'] as $member) {
if(isset($postData['admin'][$member['id']]) && $postData['admin'][$member['id']] == "on") {
$members[$member['id']] = array(
'is_admin' => 1
);
} else {
$members[$member['id']] = array(
'is_admin' => 0
);
}
}
}
/*
* Create an array of clients so we can sync the relationship easily
*
*/
if(isset($postData['clients'])) {
$clients = array();
foreach($postData['clients'] as $client) {
$clients[] = $client['id'];
}
}
/*
* Create an array of teams so we can sync the relationship easily
*
*/
if(isset($postData['teams'])) {
$teams = array();
foreach($postData['teams'] as $team) {
$teams[] = $team['id'];
}
}
/*
* Create an array of projects so we can sync the relationship easily
*
*/
if(isset($postData['projects'])) {
$projects = array();
foreach($postData['projects'] as $project) {
$projects[] = $project['id'];
}
}
if( $organisation->save() ) {
if(isset($members)) {
$organisation->users()->sync($members);
}
if(isset($teams)) {
$organisation->teams()->sync($teams);
}
if(isset($teams)) {
$organisation->clients()->sync($clients);
}
if(isset($projects)) {
$organisation->projects()->sync($projects);
}
$organisation->load('users');
$organisation->load('teams');
$organisation->load('clients');
$organisation->load('projects');
return Response::make($organisation, 200);
} else {
return Response::make("Something has gone wrong", 500);
}
}
}
I was looking a while for the problem and I didn't see any (I was looking at first sync as you suggested) but I looked again and I think the problem is not syncing users here. Probably the problem is:
if(isset($projects)) {
$organisation->projects()->sync($projects);
}
You are trying to use sync on 1 to many relationship because you defined it this way:
return $this->hasMany('Project');
So either change hasMany here into belongsToMany if it's many to many relationship (that's probably the case) or don't use sync here for $projects because it works only for many to many relationship.
I hope I can explain this clearly, apologies in advance if it is confusing. I have a goals table which hasOne of each of bodyGoalDescs, strengthGoalDescs and distanceGoalDescs as shown below
goals.php
class Goal extends BaseModel
{
protected $guarded = array();
public static $rules = array();
//define relationships
public function user()
{
return $this->belongsTo('User', 'id', 'userId');
}
public function goalStatus()
{
return $this->hasOne('GoalStatus', 'id', 'goalStatus');
}
public function bodyGoalDesc()
{
return $this->hasOne('BodyGoalDesc', 'id', 'bodyGoalId');
}
public function distanceGoalDesc()
{
return $this->hasOne('DistanceGoalDesc', 'id', 'distanceGoalId');
}
public function strengthGoalDesc()
{
return $this->hasOne('StrengthGoalDesc', 'id', 'strengthGoalId');
}
//goal specific functions
public static function yourGoals()
{
return static::where('userId', '=', Auth::user()->id)->paginate();
}
}
each of the three tables looks like this with the function details changed
class BodyGoalDesc extends BaseModel
{
protected $guarded = array();
public static $rules = array();
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'bodyGoalDescs';
//define relationships
public function goal()
{
return $this->belongsTo('Goal', 'bodyGoalId', 'id');
}
}
a goal has either a body goal, a strength goal, or a distance goal. I am having a problem with this method in the controller function
<?php
class GoalsController extends BaseController
{
protected $goal;
public function __construct(Goal $goal)
{
$this->goal = $goal;
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id)
{
$thisgoal = $this->goal->find($id);
foreach ($this->goal->with('distanceGoalDesc')->get() as $distancegoaldesc) {
dd($distancegoaldesc->DistanceGoalDesc);
}
}
}
when I pass through goal 1 which has a distance goal the above method dies and dumps the Goal object with the details of goal 1 and an array of its relations including an object with DistanceGoalDes.
when I pass through goal 2 it passes through exactly the same as if I had passed through goal 1
if I dd() $thisgoal i get the goal that was passed through
what I want ultimately is a method that returns the goal object with its relevant goal description object to the view but this wont even show me the correct goal details not too mind with the correct relations
this function is now doing what I want it to do, I am sure there is a better way (besides the fact that its happening in the controller right now) and I would love to hear it.
public function show($id)
{
$thisgoal = $this->goal->find($id);
if (!$thisgoal->bodyGoalDesc == null) {
$goaldesc = $thisgoal->bodyGoalDesc;
return View::make('goals.show')
->with('goal', $thisgoal)
->with('bodygoaldesc', $goaldesc);
} elseif (!$thisgoal->strengthGoalDesc == null) {
$goaldesc = $thisgoal->strengthGoalDesc;
return View::make('goals.show')
->with('goal', $thisgoal)
->with('strengthgoaldesc', $goaldesc);
} elseif (!$thisgoal->distanceGoalDesc == null) {
$goaldesc = $thisgoal->distanceGoalDesc;
return View::make('goals.show')
->with('goal', $thisgoal)
->with('distancegoaldesc', $goaldesc);
}
}