Laravel orderBy on a relationship - php

I am looping over all comments posted by the Author of a particular post.
foreach($post->user->comments as $comment)
{
echo "<li>" . $comment->title . " (" . $comment->post->id . ")</li>";
}
This gives me
I love this post (3)
This is a comment (5)
This is the second Comment (3)
How would I order by the post_id so that the above list is ordered as 3,3,5

It is possible to extend the relation with query functions:
<?php
public function comments()
{
return $this->hasMany('Comment')->orderBy('column');
}
[edit after comment]
<?php
class User
{
public function comments()
{
return $this->hasMany('Comment');
}
}
class Controller
{
public function index()
{
$column = Input::get('orderBy', 'defaultColumn');
$comments = User::find(1)->comments()->orderBy($column)->get();
// use $comments in the template
}
}
default User model + simple Controller example; when getting the list of comments, just apply the orderBy() based on Input::get().
(be sure to do some input-checking ;) )

I believe you can also do:
$sortDirection = 'desc';
$user->with(['comments' => function ($query) use ($sortDirection) {
$query->orderBy('column', $sortDirection);
}]);
That allows you to run arbitrary logic on each related comment record. You could have stuff in there like:
$query->where('timestamp', '<', $someTime)->orderBy('timestamp', $sortDirection);

Using sortBy... could help.
$users = User::all()->with('rated')->get()->sortByDesc('rated.rating');

Try this solution.
$mainModelData = mainModel::where('column', $value)
->join('relationModal', 'main_table_name.relation_table_column', '=', 'relation_table.id')
->orderBy('relation_table.title', 'ASC')
->with(['relationModal' => function ($q) {
$q->where('column', 'value');
}])->get();
Example:
$user = User::where('city', 'kullu')
->join('salaries', 'users.id', '=', 'salaries.user_id')
->orderBy('salaries.amount', 'ASC')
->with(['salaries' => function ($q) {
$q->where('amount', '>', '500000');
}])->get();
You can change the column name in join() as per your database structure.

I made a trait to order on a relation field. I had this issues with webshop orders that have a status relation, and the status has a name field.
Example of the situation
Ordering on with "joins" of eloquent models is not possible since they are not joins. They are query's that are running after the first query is completed. So what i did is made a lil hack to read the eloquent relation data (like table, joining keys and additional wheres if included) and joined it on the main query. This only works with one to one relationships.
The first step is to create a trait and use it on a model. In that trait you have 2 functions. The first one:
/**
* #param string $relation - The relation to create the query for
* #param string|null $overwrite_table - In case if you want to overwrite the table (join as)
* #return Builder
*/
public static function RelationToJoin(string $relation, $overwrite_table = false) {
$instance = (new self());
if(!method_exists($instance, $relation))
throw new \Error('Method ' . $relation . ' does not exists on class ' . self::class);
$relationData = $instance->{$relation}();
if(gettype($relationData) !== 'object')
throw new \Error('Method ' . $relation . ' is not a relation of class ' . self::class);
if(!is_subclass_of(get_class($relationData), Relation::class))
throw new \Error('Method ' . $relation . ' is not a relation of class ' . self::class);
$related = $relationData->getRelated();
$me = new self();
$query = $relationData->getQuery()->getQuery();
switch(get_class($relationData)) {
case HasOne::class:
$keys = [
'foreign' => $relationData->getForeignKeyName(),
'local' => $relationData->getLocalKeyName()
];
break;
case BelongsTo::class:
$keys = [
'foreign' => $relationData->getOwnerKeyName(),
'local' => $relationData->getForeignKeyName()
];
break;
default:
throw new \Error('Relation join only works with one to one relationships');
}
$checks = [];
$other_table = ($overwrite_table ? $overwrite_table : $related->getTable());
foreach($keys as $key) {
array_push($checks, $key);
array_push($checks, $related->getTable() . '.' . $key);
}
foreach($query->wheres as $key => $where)
if(in_array($where['type'], ['Null', 'NotNull']) && in_array($where['column'], $checks))
unset($query->wheres[$key]);
$query = $query->whereRaw('`' . $other_table . '`.`' . $keys['foreign'] . '` = `' . $me->getTable() . '`.`' . $keys['local'] . '`');
return (object) [
'query' => $query,
'table' => $related->getTable(),
'wheres' => $query->wheres,
'bindings' => $query->bindings
];
}
This is the "detection" function that reads the eloquent data.
The second one:
/**
* #param Builder $builder
* #param string $relation - The relation to join
*/
public function scopeJoinRelation(Builder $query, string $relation) {
$join_query = self::RelationToJoin($relation, $relation);
$query->join($join_query->table . ' AS ' . $relation, function(JoinClause $builder) use($join_query) {
return $builder->mergeWheres($join_query->wheres, $join_query->bindings);
});
return $query;
}
This is the function that adds a scope to the model to use within query's. Now just use the trait on your model and you can use it like this:
Order::joinRelation('status')->select([
'orders.*',
'status.name AS status_name'
])->orderBy('status_name')->get();

Related

laravel / Mongodb - How to insert new key/ value to an existing document

I have this document:
Now I want to add a new key / value to that document which id is : 34401
My key / value is like an array:
$data = [
'id_project' => $id_project,
'id_product' => $product_id,
'link' => $link,
'id_product_competitor' => $id_competitor,
'link_competitor' => ''
];
what I am doing is this:
$insert_to_mongodb = DB::connection('mongodb')
->collection( 'products_' . $id_project . '_' . $id_competitor )
->insert($data);
Here this 'products_' . $id_project . '_' . $id_competitor is products_1_23 as collection name.
After run this code Its inserting newly document but I want to add ths key/value to an existing document.
I belive the better way is to create a model for the mongodb collection,
install Jenssegers\Mongodb using the command composer require jenssegers/mongodb, reference link
<?php
namespace App\Models;
use Jenssegers\Mongodb\Eloquent\Model as Eloquent;
class ProductsModel extends Eloquent{
/**
* Connection name
*
*/
protected $connection = 'mongodb';
/**
* Collection name
*
*/
protected $collection = 'products';
}
Now you can insert or update using the eloquent way
ProductsModel::insert($data);
ProductsModel::where('_id',$id)->update($data);
Well, I have soleve it by following way:
$insert_to_mongodb = DB::connection('mongodb')
->collection( 'products_' . $id_project . '_' . $id_competitor )
->where('id', (int) $product_id)
->update($data, ['upsert' => true]);

Laravel Query Builder Join vs With Function, Which One is Better?

I want to ask about Laravel Query using Join or With which is better.
In this case there is a short query that I have tried. But there are some things that make me wonder.
In my case, I'm trying to create a list of users using the API. The problem lies in sorting the data.
The problem is divided into several.
If I use With.
The advantage of using with is that I can call the attributes in the model without rewriting the attributes I want to use. But I was confused when calling data related to other tables for me to sort. example query:
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$sortBy = $request->query('sortBy');
$sortDesc = (is_null($request->query('sortDesc'))) ? $request->query('sortDesc') : ($request->query('sortDesc') == 'true' ? 'desc' : 'asc');
$page = $request->query('page');
$itemsPerPage = $request->query('itemsPerPage');
$search = $request->query('search');
$starDate = $request->query('start');
$endDate = $request->query('end');
$start = ($page - 1) * $itemsPerPage;
$query = MemberRegular::query();
$query->with(['users' => function ($subQuery) {
$subQuery->select('id', 'name', 'email', 'phone');
}]);
$query->select(
'id',
'code'
);
if ($search) {
$query->where(function ($subQuery) use ($search) {
$subQuery->where('code', 'like', '%' . $search . '%');
$subQuery->orWhere(function ($q) use ($search) {
$q->whereHas('users', function ($j) use ($search) {
$j->where('name', 'like', '%' . $search . '%');
$j->orWhere('email', 'like', '%' . $search . '%');
})
});
});
}
if ($sortBy && $sortDesc) {
$query->orderBy($sortBy, $sortDesc)->orderBy('id', 'desc');
} else {
$query->orderBy('created_at', 'desc')->orderBy('id', 'desc');
}
if ($starDate && $endDate) {
$query->whereBetween('created_at', [$starDate, $endDate]);
}
$data['totalItems'] = $query->count();
$data['items'] = $query->skip($start)->take($itemsPerPage)->get();
return HResource::collection($data['items'])->additional(['totalItems' => (int) $data['totalItems']], true);
}
If I use Join.
The advantage of using Join is that I can sort data easily if the data is related to other tables. But I have to re-create a new attribute in a collection. example query:
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$sortBy = $request->query('sortBy');
$sortDesc = (is_null($request->query('sortDesc'))) ? $request->query('sortDesc') : ($request->query('sortDesc') == 'true' ? 'desc' : 'asc');
$page = $request->query('page');
$itemsPerPage = $request->query('itemsPerPage');
$search = $request->query('search');
$starDate = $request->query('start');
$endDate = $request->query('end');
$start = ($page - 1) * $itemsPerPage;
$query = MemberRegular::query();
$query->join('users', 'users.id', '=', 'member_regulars.user_id');
$query->select(
'member_regulars.id',
'member_regulars.code',
'users.name',
'users.email',
'users.phone'
);
if ($search) {
$query->where(function ($subQuery) use ($search) {
$subQuery->where('member_regulars.code', 'like', '%' . $search . '%');
$subQuery->orWhere('users.name', 'ilike', '%' . $search . '%');
$subQuery->orWhere('users.email', 'ilike', '%' . $search . '%');
$subQuery->orWhere('users.phone', 'ilike', '%' . $search . '%');
});
}
if ($sortBy && $sortDesc) {
$query->orderBy($sortBy, $sortDesc)->orderBy('member_regulars.id', 'desc');
} else {
$query->orderBy('member_regulars.created_at', 'desc')->orderBy('member_regulars.id', 'desc');
}
if ($starDate && $endDate) {
$query->whereBetween('member_regulars.created_at', [$starDate, $endDate]);
}
$data['totalItems'] = $query->count();
$data['items'] = $query->skip($start)->take($itemsPerPage)->get();
return HResource::collection($data['items'])->additional(['totalItems' => (int) $data['totalItems']], true);
}
If using Query With The problem lies in sending the sortBy parameter like the following users.name it will be an error because the table is not found in the query I made, but I can immediately call attributes that can be used directly without needing to create a new custom attribute.
If using Query Join, the problem is that I have to re-create custom attributes to be used in data collections, but I don't need to worry about sorting data.
Both are equally important to me. However, if anyone is willing to give advice on the best way I have to use Join or With for this case.
Thank you.
Finally I found the best solution to the problem I was facing. I hope this can help others.
Here I choose to use Join why? because it turns out that I can call the function relations users() in the model that I created so that I can still retrieve custom attributes in the Users model. I don't really know if this is the right way or not. I hope this helps others.
Thank you.

PHP / Laravel - Combine result (Model relationship)

Right now I have below data:
Now the problem with above is, that it's creating a new array for each entry in my database.
I would very much like to get below output:
The difference is here that:
All the routes that are the same, should be grouped. So if the Route is the same, it should be under the same route list.
If the Carrier under the route is the same, then it should be under that route and carrier
If the route is different, it should start a new list (**Route: XYZ to XYZ)
Current Code:
Controller.php:
public function show()
{
$routes = Routes::where('id', '>=', 1)
->with('routeStops', 'getVessel')
->get()
->toArray();
foreach ($routes as $key => $value) {
echo '<h2>Route: ' . $value['origin'] . ' to ' . $value['destination'] . '</h2>';
$carrier = Carriers::where('id', $value['get_vessel']['carriers_id'])->first()->name;
echo 'Carrier: ' . $carrier . '';
dump($value);
}
}
This is my models:
Routes.php
/**
* Get the route stop associated with this route.
*/
public function routeStops()
{
return $this->hasMany('App\RouteStops', 'route_id');
}
/**
* Get the vessel record associated with this route.
*/
public function getVessel()
{
return $this->belongsTo('App\Vessels', 'vessel_id');
}
How can I do so I achieve the 2nd desired output?

Laravel loop through query result, then paginate

I'm working on an event booking system. When the user searches for a date range, I use a function inside my Event model that checks if the event is available. My search function currently works as follows:
$events = Event::where('name', 'LIKE', '%' . $request->get('term') . '%')
->where('accepted',1)
->orWhere('description', 'LIKE', '%'.$request->get('term').'%')
->orWhere('city', 'LIKE', '%'.$request->get('term').'%')->paginate(15);
$availableEvents = new Collection();
if ($request->get('from') !== '' AND $request->get('to') !== '') {
foreach($events as $key => $event) {
if ($event->is_available($request->get('from'), $request->get('to'))) {
$availableEvents->add($event);
}
}
}
else {
$availableEvents = $events;
}
return view('frontend.events.results', ['events' => $availableEvents, 'request' => $request]);
I can't check availability inside the query builder so I have to loop through. I'm using a Illuminate\Database\Eloquent\Collection because I can't remove events from the paginated thing I get from the query builder.
Is there some way to convert the Eloquent Collection to a paginated collection?
Thanks in advance!
P.S. I'm passing $request to the view to use appends() so I can retain the query string parameters during pagination.
EDIT: The is_available function
public function is_available($from, $to) {
$from = Carbon::createFromFormat('d-m-Y', $from);
$to = Carbon::createFromFormat('d-m-Y', $to);
foreach($this->not_availables as $notAvailable) {
$notAvailableFrom = Carbon::createFromFormat('Y-m-d', $notAvailable->from);
$notAvailableTo = Carbon::createFromFormat('Y-m-d', $notAvailable->to);
if ($from->gte($notAvailableFrom) OR $to->lte($notAvailableTo)) {
return false;
}
}
return true;
}
You can manually create a paginator
You could use Illuminate\Pagination\LengthAwarePaginator like this:
use \Illuminate\Pagination\LengthAwarePaginator;
...
$page = $request->get('page', 1);
$limit = 10;
$paginator = new LengthAwarePaginator(
$availableEvents->forPage($page, $limit), $availableEvents->count(), $limit, $page, ['path' => $request->path()]
);
Then pass the $paginator along with your view, instead of your $availableEvents. In your view you still have $events, you can render the pagination with $events->render(); and pass on the route parameters from $request as you normally would.

Laravel Unit Testing, how to "seeInDatabase" soft deleted row?

I'm working on a small unit test where I soft delete a row. To mark the test as successful I have to find that row with:
a given ID and
deleted_at column should not be null.
I can fulfil first condition - because obviously I know the ID.
Unfortunately I don't know how to tell seeInDatabase method that I expect deleted_at not to be null:
$this->seeInDatabase(
'diary_note_categories',
[
'id' => 'a7e35ad0-6f00-4f88-b953-f498797042fc',
'deleted_at' => null // should be is not null, like <> or != or whatever
]
);
Any hints?
'deleted_at <>' => null breaks
'deleted_at' => ['!=' => null] breaks as well
I did it in this way:
$this->seeInDatabase('diary_note...',['id' => 'a7e35ad0'])
->notSeeInDatabase('diary_note...',['id' => 'a7e35ad0','deleted_at'=>null]);
So I'm checking in two steps
I check if there is a record with our id in the table
I check if there is no record with our id and deleted_at = null in the table
It's not currently possible. Both seeInDatabase and notSeeInDatabase just pass the array directly to the where method of the query builder and that doesn't understand how to deal with anything other than = when passed an array.
https://github.com/laravel/framework/blob/2b4b3e3084d3c467f8dfaf7ce5a6dc466068b47d/src/Illuminate/Database/Query/Builder.php#L452
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
// If the column is an array, we will assume it is an array of key-value pairs
// and can add them each as a where clause. We will maintain the boolean we
// received when the method was called and pass it into the nested where.
if (is_array($column)) {
return $this->whereNested(function ($query) use ($column) {
foreach ($column as $key => $value) {
$query->where($key, '=', $value);
}
}, $boolean);
}
// ...
}
Option 1 - Add the following code to your TestCase class which you extend your test cases from
Gist: https://gist.github.com/EspadaV8/73c9b311eee96b8e8a03
<?php
/**
* Assert that a given where condition does not matches a soft deleted record
*
* #param string $table
* #param array $data
* #param string $connection
* #return $this
*/
protected function seeIsNotSoftDeletedInDatabase($table, array $data, $connection = null)
{
$database = $this->app->make('db');
$connection = $connection ?: $database->getDefaultConnection();
$count = $database->connection($connection)
->table($table)
->where($data)
->whereNull('deleted_at')
->count();
$this->assertGreaterThan(0, $count, sprintf(
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));
return $this;
}
/**
* Assert that a given where condition matches a soft deleted record
*
* #param string $table
* #param array $data
* #param string $connection
* #return $this
*/
protected function seeIsSoftDeletedInDatabase($table, array $data, $connection = null)
{
$database = $this->app->make('db');
$connection = $connection ?: $database->getDefaultConnection();
$count = $database->connection($connection)
->table($table)
->where($data)
->whereNotNull('deleted_at')
->count();
$this->assertGreaterThan(0, $count, sprintf(
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));
return $this;
}
Option 2 - Install the following composer package
This composer package is the exact same code as above, but packaged up for Composer.
composer require kirkbater/soft-deletes
Then use it inside of your specific test class:
<?php
use Kirkbater\Testing\SoftDeletes;
class MyTestClass extends TestClass {
use SoftDeletes;
}
This is an old question, but for those using more recent versions of Laravel (5.4 and above), there is now an assertSoftDeleted assertion: documentation.
So the answer to the original question would now be:
$this->assertSoftDeleted('diary_note_categories', [
'id' => 'a7e35ad0-6f00-4f88-b953-f498797042fc'
]);
Assert the given record has been deleted (Laravel 5.4 and above).
assertSoftDeleted(string|Model $table, array $data = [], string|null $connection = null)
Example with id:
$this->assertSoftDeleted('table_name', ['id'='value'])
Example with model:
$user = User::factory()->create();
$user->delete();
$this->assertSoftDeleted($user);
I used in Laravel 6
$this->assertDatabaseMissing('stores', [
'id' => $test_data['store']->id, 'deleted_at' => null
]);
$this->assertDatabaseHas('stores', ['id' => $id]);
It is not tested, but try like this :
$this->seeInDatabase(
'diary_note_categories',
[
'id' => 'a7e35ad0-6f00-4f88-b953-f498797042fc',
'deleted_at' => ['deleted_at' ,'!=', null ] // should be is not null, like <> or != or whatever
]
);

Categories