CakePHP pagination don't sort with custom query - sorting

In my CakePHP project I need to get data with custom query and paginate this. I succeed with a custom pagination function like this:
public function paginate($conditions, $fields, $order, $limit, $page = 1, $recursive = null, $extra = array()) {
$recursive = -1;
$sql = "
SELECT Post.*, ....
FROM
(SELECT ....) //this is my problem
AS Post1
LEFT JOIN ...
LEFT JOIN ...
LEFT JOIN
LEFT JOIN
LEFT JOIN
GROUP BY Post1.id
ORDER BY ...
LIMIT " . (($page - 1) * $limit) . ', ' . $limit;
$results = $this->query($sql);
return $results;
}
The result that I want is correct, the Paginator divides correctly the row according to the limit value for one page.
But the sort function:
echo $this->Paginator->sort('id', 'ID'); //don't works
if I don't use a custom paginator function, this works fine but I can't retrieve my specific data like custom query.
It's possible with this settings example:
$this->paginate = array(
'limit' => 10,
'fields' => array('Post.id', 'Post.title', 'Post.created'),
'conditions' => array('Post.title LIKE' => '%search_keyword%'),
'order' => array('Post.title' => 'asc', 'Post.id' => 'asc')
);
run a custom query like this:
SELECT Post.*, ....
FROM
(SELECT ../*complex query*/...) // <--this is my problem!!
AS Post1
LEFT JOIN ...
Thanks

Related

Laravel order by eagerly loaded column

I am using laravel eager loading to load data on the jquery datatables. My code looks like:
$columns = array(
0 => 'company_name',
1 => 'property_name',
2 => 'amenity_review',
3 => 'pricing_review',
4 => 'sqft_offset_review',
5 => 'created_at',
6 => 'last_uploaded_at'
);
$totalData = Property::count();
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$dir = $request->input('order.0.dir');
$query = Property::with(['company','notices']);
$company_search = $request->columns[0]['search']['value'];
if(!empty($company_search)){
$query->whereHas('company', function ($query) use($company_search) {
$query->where('name','like',$company_search.'%');
});
}
$property_search = $request->columns[1]['search']['value'];
if(!empty($property_search)){
$query->where('properties.property_name','like',$property_search.'%');
}
if(!Auth::user()->hasRole('superAdmin')) {
$query->where('company_id',Auth::user()->company_id);
}
$query->orderBy($order,$dir);
if($limit != '-1'){
$records = $query->offset($start)->limit($limit);
}
$records = $query->get();
With this method I received error: Column not found: 1054 Unknown column 'company_name' in 'order clause' .
Next, I tried with following order condition:
if($order == 'company_name'){
$query->orderBy('company.name',$dir);
}else{
$query->orderBy($order,$dir);
}
However, it also returns similar error: Column not found: 1054 Unknown column 'company.name' in 'order clause'
Next, I tried with whereHas condition:
if($order == 'company_name'){
$order = 'name';
$query->whereHas('company', function ($query) use($order,$dir) {
$query->orderBy($order,$dir);
});
}else{
$query->orderBy($order,$dir);
}
But, in this case also, same issue.
For other table, I have handled this type of situation using DB query, however, in this particular case I need the notices as the nested results because I have looped it on the frontend. So, I need to go through eloquent.
Also, I have seen other's answer where people have suggested to order directly in model like:
public function company()
{
return $this->belongsTo('App\Models\Company')->orderBy('name');
}
But, I don't want to order direclty on model because I don't want it to be ordered by name everytime. I want to leave it to default.
Also, on some other scenario, I saw people using join combining with, but I am not really impressed with using both join and with to load the same model.
What is the best way to solve my problem?
I have table like: companies: id, name, properties: id, property_name, company_id, notices: title, slug, body, property_id
The issue here is that the Property::with(['company','notices']); will not join the companies or notices tables, but only fetch the data and attach it to the resulting Collection. Therefore, neither of the tables are part of the SQL query issued and so you cannot order it by any field in those tables.
What Property::with(['company', 'notices'])->get() does is basically issue three queries (depending on your relation setup and scopes, it might be different queries):
SELECT * FROM properties ...
SELECT * FROM companies WHERE properties.id in (...)
SELECT * FROM notices WHERE properties.id in (...)
What you tried in the sample code above is to add an ORDER BY company_name or later an ORDER BY companies.name to the first query. The query scope knows no company_name column within the properties table of course and no companies table to look for the name column. company.name will not work either because there is no company table, and even if there was one, it would not have been joined in the first query either.
The best solution for you from my point of view would be to sort the result Collection instead of ordering via SQL by replacing $records = $query->get(); with $records = $query->get()->sortBy($order, $dir);, which is the most flexible way for your task.
For that to work, you would have to replace 'company_name' with 'company.name' in your $columns array.
The only other option I see is to ->join('companies', 'companies.id', 'properties.company_id'), which will join the companies table to the first query.
Putting it all together
So, given that the rest of your code works as it should, this should do it:
$columns = [
'company.name',
'property_name',
'amenity_review',
'pricing_review',
'sqft_offset_review',
'created_at',
'last_uploaded_at',
];
$totalData = Property::count();
$limit = $request->input('length');
$start = $request->input('start');
$order = $columns[$request->input('order.0.column')];
$dir = $request->input('order.0.dir');
$query = Property::with(['company', 'notices']);
$company_search = $request->columns[0]['search']['value'];
$property_search = $request->columns[1]['search']['value'];
if (!empty($company_search)) {
$query->whereHas(
'company', function ($query) use ($company_search) {
$query->where('name', 'like', $company_search . '%');
});
}
if (!empty($property_search)) {
$query->where('properties.property_name', 'like', $property_search . '%');
}
if (!Auth::user()->hasRole('superAdmin')) {
$query->where('company_id', Auth::user()->company_id);
}
if ($limit != '-1') {
$records = $query->offset($start)->limit($limit);
}
$records = $query->get()->sortBy($order, $dir);

Perform order by relationship field in Eloquent

I want to create product filter with Eloquent.
I start like this
$query = Product::whereHas('variants')
->with('variants')
->with('reviews')
$query = $this->addOrderConstraints($request, $query);
$products = $query->paginate(20);
Where
private function addOrderConstraints($request, $query)
{
$order = $request->input('sort');
if ($order === 'new') {
$query->orderBy('products.created_at', 'DESC');
}
if ($order === 'price') {
$query->orderBy('variants.price', 'ASC');
}
return $query;
}
However, that doesn't work, cause Eloquent is performing this query like this (information from Laravel DebugBar)
select count(*) as aggregate from `products` where exists
(select * from `variants` where `products`.`id` = `variants`.`product_id`)
select * from `products` where exists
(select * from `variants` where `products`.`id` = `variants`.`product_id`)
select * from `variants` where `variants`.`product_id` in ('29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48')
And so on
So when I try to use sorting by price it just obvious error
Unknown column 'variants.price' in 'order clause' (SQL: select * from
`products` where exists (select * from `variants` where `products`.`id` =
variants.product_id) order by variants.price asc limit 20 offset 0)
So is it possible to perform relationship ordering with Eloquent or not?
This will sort the subquery. Not the "first query (the product query)".
Basically, your subquery will be:
select * from variants where product_id in (....) order by price, and that is not what you want, right?
<?php
// ...
$order = $request->sort;
$products = Product::whereHas('variants')->with(['reviews', 'variants' => function($query) use ($order) {
if ($order == 'price') {
$query->orderBy('price');
}
}])->paginate(20);
If you want to sort product +/or variant you need to use join.
$query = Product::select([
'products.*',
'variants.price',
'variants.product_id'
])->join('variants', 'products.id', '=', 'variants.product_id');
if ($order == 'new') {
$query->orderBy('products.created_at', 'DESC');
} else if ($order == 'price') {
$query->orderBy('variants.price');
}
return $query->paginate(20);
If you want to sort product and variants, you don't need joins, because you won't have the related model loaded (like $product->variants), just all the fields of the variants table.
To sort models by related submodels, we can use Eloquent - Subquery Ordering.
To order the whole model by a related model, and NOT the related model itself, we can do it like this:
return Product::with('variants')->orderBy(
Variants::select('price')
// This can vary depending on the relationship
->whereColumn('variant_id', 'variants.id')
->orderBy('price')
->limit(1)
)->get();

Laravel Eloquent or Query builder : Use OR conditions inside of AND Conditions

Data flow :
4-5 Main Categories and each main category have 10-15 sub categories.
Few services which belongs to few categories. Categories are stored in services table as serialized data like this :
a:2:{i:0;s:1:"1";i:1;s:2:"3";}
a:1:{i:0;s:3:"2";}
a:1:{i:0;s:3:"3";}
a:2:{i:0;s:1:"1";i:1;s:3:"3";}
Posted form data are as below :
array(
[0] => array(
[0] => 1,
[1] => 3,
),
[1] => array(
[0] => 2
),
....
....
....
)
Where each selected categories which are in same main category are grouped in an array.Also the number of categories and subcategories are not fix
Plain SQL to get records
SELECT * FROM `services` WHERE (`categories` LIKE '%"1"%' OR `categories` LIKE '%"3"%') AND (`categories` LIKE '%"2"%') AND ( ... OR ...) AND ( ... OR ...) ....
I've tied this but this seems to be not working.
\DB::table ('services')
->where(function($q) use ($categories) {
foreach($categories as $category) {
$q->where('services.categories', 'like', '%"'.DB::raw($category).'"%');
}
})
The query in Laravel with query builder
$queryBuilder = \DB::table('services');
//loop to create dynamic where query
foreach($categories as $category){
//create a query as (condition OR condition)
$queryBuilder->where(function($query) use($category) {
$query->where('categories', 'LIKE', "%" . $category[0] . "%");
//if count is two then adding the or where clause
if(count($category) === 2){
$query->orWhere('categories', 'LIKE', "%" . $category[1] . "%");
}
});
}
}
$result = $queryBuilder->get(); //executing the query to get result

Laravel how do I get the row number of an object using Eloquent?

I'd like to know the position of a user based on its creation date. How do I do that using Eloquent?
I'd like to be able to do something like this:
User::getRowNumber($user_obj);
I suppose you want MySQL solution, so you can do this:
DB::statement(DB::raw('set #row:=0'));
User::selectRaw('*, #row:=#row+1 as row')->get();
// returns all users with ordinal 'row'
So you could implement something like this:
public function scopeWithRowNumber($query, $column = 'created_at', $order = 'asc')
{
DB::statement(DB::raw('set #row=0'));
$sub = static::selectRaw('*, #row:=#row+1 as row')
->orderBy($column, $order)->toSql();
$query->remember(1)->from(DB::raw("({$sub}) as sub"));
}
public function getRowNumber($column = 'created_at', $order = 'asc')
{
$order = ($order == 'asc') ? 'asc' : 'desc';
$key = "userRow.{$this->id}.{$column}.{$order}";
if (Cache::get($key)) return Cache::get($key);
$row = $this->withRowNumber($column, $order)
->where($column, '<=',$this->$column)
->whereId($this->id)->pluck('row');
Cache::put($key, $row);
return $row;
}
This needs to select all the rows from the table till the one you are looking for is found, then selects only that particular row number.
It will let you do this:
$user = User::find(15);
$user->getRowNumber(); // as default ordered by created_at ascending
$user->getRowNumber('username'); // check order for another column
$user->getRowNumber('updated_at', 'desc'); // different combination of column and order
// and utilizing the scope:
User::withRowNumber()->take(20)->get(); // returns collection with additional property 'row' for each user
As this scope requires raw statement setting #row to 0 everytime, we use caching for 1 minute to avoid unnecessary queries.
$query = \DB::table(\DB::raw('Products, (SELECT #row := 0) r'));
$query = $query->select(
\DB::raw('#row := #row + 1 AS SrNo'),
'ProductID',
'ProductName',
'Description',
\DB::raw('IFNULL(ProductImage,"") AS ProductImage')
);
// where clauses
if(...){
$query = $query->where('ProductID', ...));
}
// orderby clauses
// ...
// $query = $query->orderBy('..','DESC');
// count clause
$TotalRecordCount = $query->count();
$results = $query
->take(...)
->skip(...)
->get();
I believe you could use Raw Expresssions to achieve this:
$users = DB::table('users')
->select(DB::raw('ROW_NUMBER() OVER(ORDER BY ID DESC) AS Row, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
However, looking trough the source code looks like you could achieve the same when using SQLServer and offset. The sources indicates that if you something like the following:
$users = DB::table('users')->skip(10)->take(5)->get();
The generated SQL query will include the row_number over statement.
[For Postgres]
In your model
public function scopeWithRowNumber($query, $column = 'id', $order = 'asc'){
$sub = static::selectRaw('*, row_number() OVER () as row_number')
->orderBy($column, $order)
->toSql();
$query->from(DB::raw("({$sub}) as sub"));
}
In your controller
$user = User::withRowNumber()->get();

Get product's category ids and names

There is this method getCategoryIds() in Mage_Catalog_Model_Resource_Eav_Mysql4_Product. This method returns all category IDs of requested product. I need to modify the SELECT statement so it will return also category names.
Here is the basic query:
$select = $this->_getReadAdapter()->select()
->from($this->_productCategoryTable, 'category_id')
->where('product_id=?', $product->getId());
I can't use catalog_category_flat table to for some reasons, so I have to use EAV tables. So at this point I have this query:
$select = $this->_getReadAdapter()->select()
->from($this->_productCategoryTable, 'category_id')
->where('catalog_category_product.product_id=?', $product->getId())
->join(
array('a' =>'catalog_category_entity_varchar'),
'a.entity_id = catalog_category_product.category_id',
array('name' => 'value')
)
->join(
array('b' => $this->getTable('eav/attribute')),
'b.attribute_id = a.attribute_id',
array()
)
->where("b.attribut_code = 'name'");
This works, but I'd like to ask if there is a better way of doing it.
Easiest and probably cleanest:
$categories = $product->getCategoryCollection()
->addAttributeToSelect('name');
Then you can simply traverse the collection:
foreach($categories as $category) {
var_dump($category->getName());
}
I found this post: Joining an EAV table and made the function, which will add the category join query, use it from the collection.
Just call this function from the code this way:
// To join the name of category
$this->joinCategoryAttribute('<your table alias>.category_id', 'name');
And this is the function:
public function joinCategoryAttribute($joinOriginId, $code)
{
$attributeId = Mage::getResourceModel('eav/entity_attribute')->getIdByCode('catalog_category', $code);
$entityType = Mage::getModel('eav/entity_type')->loadByCode('catalog_category');
$attribute = Mage::getModel($entityType->getAttributeModel())->load($attributeId);
$entityTable = $this->getTable($entityType->getEntityTable());
$alias = 'table' . $code;
$table = $entityTable . '_' . $attribute->getBackendType();
$field = $alias . '.value';
$this->getSelect()
->joinLeft(array($alias => $table),
"{$joinOriginId} = {$alias}.entity_id AND {$alias}.attribute_id = " . $attribute->getAttributeId(),
array($attribute->getAttributeCode() => $field)
);
return $this;
}

Resources