How to programmatically get relationship data without having a model instance? - laravel

I have the following working code that gives me a collection of a model type that each have any of the given relationship values (like a tag with id 1, 2 or 3):
<?php
public function getEntitiesWithRelationValues($entityType, $relations = []) {
$related = new EloquentCollection();
$locale = App::getLocale();
$entityType = new $entityType(); // bad?
// $entityType = new ReflectionClass($entityType); // not working
foreach ($relations as $relation => $modelKeys) {
if ($entityType->{$relation}()->exists()) {
$relatedClass = get_class($entityType->{$relation}()->getRelated());
$relationPrimaryKeyName = ($instance = new $relatedClass)->getQualifiedKeyName();
$relationEntities = $entityType::where('published->' . $locale, true)
->whereHas($relation, function (Builder $query) use($modelKeys, $relationPrimaryKeyName) {
$query->whereIn($relationPrimaryKeyName, $modelKeys);
})
->get()
->sortKeysDesc()
->take(10)
;
$related = $related->concat($relationEntities->except($related->modelKeys()));
}
}
return $related;
}
I feel $entityType = new $entityType(); is bad code because I dont want to create a new model. The reflection class throws the error "ReflectionClass undefined method {$relation}". How can I get the relationship data of a model type without actually loading/ instantiating a model?
A few weeks ago I asked something similar here but in that case I did have a model loaded.

You could consider the following solution:
Actually use a Model instance as the input to your getEntitiesWithRelationValues() function, since one way or another you are going to retrieve the relationships for a specific instance right?
Create a property for your model:
public static $relationNames = ['tags', 'posts', 'comments'];
Retrieve that property in your code using MyModel::$relationNames and call whichever functions you need.
Alternative solution using reflection
The snippet below uses reflection to find all public methods of User::class, then filter return types based on if it is a subclass of \Illuminate\Database\Eloquent\Relations\Relation. Note that you need to annotate the return types specifically using the new language struct like public function myMethod() : MyReturnType { }.
use App\Models\User;
use Illuminate\Database\Eloquent\Relations\Relation;
$relationshipMethods = collect(
(new ReflectionClass(User::class))
->getMethods(\ReflectionMethod::IS_PUBLIC)
)
->filter(function(\ReflectionMethod $method) {
// The `?` makes the code work for methods without an explicit return type.
return is_subclass_of($method->getReturnType()?->getName(), Relation::class);
})->values();
// $relationshipMethods now is a collection with \ReflectionMethod objects.

Related

use indirect relation when intermediate model is empty

i have made indirect relation from one model to another in couple of my models.
this is my Work Model:
public function GeoEntities()
{
return $this->hasMany(\App\GeoEntity::class);
}
public function geoLand()
{
$builder = $this->GeoEntities()->where("entity_type", 0);
$relation = new HasOne($builder->getQuery(), $this, 'work_id', 'id');
return $relation;
}
public function geoLandPoints()
{
return $this->geoLand->geoPoints();
}
this return $this->intermediateModel->FinalModel(); would work, if intermediate relation is belongsTo() and returns a relation instance.
but in this case, when geoLand is Empty it produce error:
Call to a member function geoPoints() on null
like below line:
$points = $work->geoLandPoints;
The Intermediate Relation is a hasMany
i want to have this like relation call geoLandPoints and not geoLandPoints() but,
when intermidate models are null, i want an empty relation.
but i can not figure it out, how to achieve this.
with Fico7489\Laravel\EloquentJoin\Traits\EloquentJoin
using Fico7489\Laravel\EloquentJoin\Traits\EloquentJoin package, i have tried to refactor relation like below:
public function geoLandPoints()
{
$builder = $this
->select("works.*")
->join("geo_entities", "works.id", "geo_entities.work_id")
->join("geo_points", "geo_entities.id", "geo_points.geo_entity_id")
->where("entity_type", 0)
->where("works.id", $this->id);
return new HasMany($builder->getQuery(), $this, "work_id", "id");
}
but it couldn't convert Database Query Builder to Eloquent Query Builder.
Argument 1 passed to
Illuminate\Database\Eloquent\Relations\HasOneOrMany::__construct()
must be an instance of Illuminate\Database\Eloquent\Builder, instance
of Illuminate\Database\Query\Builder given
Why don't you use the hasOne() method instead of trying to return your own HasOne class? Also, you can use withDefault() so the relationship returns an empty GeoEntity instead of null.
public function geoLand()
{
return $this->hasOne(\App\GeoEntity::class)->where("entity_type", 0)->withDefault();
}
You could even pass an array of default values. withDefault(['column' => 'value', 'column2' => 'value2', ...])

Query Inside a redis model in yii2

I have created a redis model, which should store statistics like this:
<?php
namespace app\models;
use Yii;
use yii\base\Model;
use \yii\redis\ActiveRecord;
use \yii\redis\ActiveQuery;
class StatsModel extends ActiveRecord
{
public function attributes()
{
return ['id', 'hits', 'user', 'ua', 'ip','os'];
}
public function rules()
{
return [
['user', 'required'],
['user','string'],
['ip', 'required'],
['ip', 'integer'],
['hits', 'integer'],
['ua','string'],
['os','integer']
];
}
public static function total_user_hits($username)
{
$query = new ActiveQuery($this);
$query->find()->where('user = '.$user)->all();
}
public static function getDb()
{
return \Yii::$app->db_redis;
}
}
Now, i'm trying to make a static function, which i can use, to count all thi hits value for specific user in redis. I'm creating an $query = new ActiveQuery($this); each time in the function, but can how can initiliase just one copy of the query to always use it? If i do it like class property:
public $query = new ActiveQuery($this);
I get error expression is not allowed as field default value
You should not reuse existing query object (unless you want to make query with the same conditions) - ActiveQuery is mutable, it means that previous queries may change its state:
$query = new ActiveQuery(StatsModel::class);
$result1 = $query->andWhere('user = 1')->all(); // 1 result
$result2 = $query->andWhere('user = 2')->all(); // no results
Second query will not return anything, since it will create condition like WHERE user = 1 AND user = 2 which is always false.
If you're afraid about performance, you should not. Creating ActiveQuery object has negligible overhead. Creating objects in PHP is relatively cheap and ActiveQuery is quite lightweight - the most time consuming thing will be actual query to redis/db.

Doing ->save to multiple relations

I am using Laravel 5.5.13.
I have a model called Thumb. This thumb is related to two things in a many-to-one relationship: Player and Comment.
I currently do things like this:
public function store(Request $request, Entity $entity, Player $player)
{
$thumb = new Thumb($request->all());
$thumb->player_id = $player->id;
$entity->thumbs()->save($thumb);
return response()->json($thumb, 201);
}
We see how I have to set $thumb->player_id AND I don't have to set the entity_id because I am doing $entity->thumbs()->save
Is there a way to do $entityAndPlayer->thumbs()->save? Or is the way I did it above the recommend way?
You cannot use relationships to set 2 foreign columns so they way you showed is the correct one. However you can make it a bit cleaner in my opinion.
Into Thumb model you could add:
public function setPlayer(Player $player)
{
$this->player_id = $player->id;
}
and then instead of:
$thumb->player_id = $player->id;
you could use:
$thumb->setPlayer($player);
Or you could add create setPlayerAttribute method and finally instead of:
$thumb = new Thumb($request->all());
$thumb->player_id = $player->id;
use just:
$thumb = new Thumb($request->all() + ['player' => $player]);
You can't save multiple relationships at once but, for many to one associations you can use the method associate() (Laravel docs) to save using the belongs to part of the relationship, for example:
public function store(Request $request, Entity $entity, Player $player)
{
$thumb = new Thumb($request->all());
$thumb = $thumb->save();
$thumb->player()->associate($player);
$thumb->entity()->associate($entity);
return response()->json($thumb, 201);
}

Can I filter laravel collection using where method and some other Model's method?

I have a model called Billboard. In this model I wrote a method isDisplayable().
public function isDisplayable()
{
if (/* Logic to determine if billboard is displayable */)
return true;
return false;
}
I want the collection of Billboards that are only displayable. Can I utilize Where method with isDisplayable()? or should I adopt some other approach?
If you already have the Collection, you can use the filter() method to filter out results based on your Model method:
$billboards = Billboard::all();
$filtered = $billboards->filter(function ($billboard, $key) {
// true to keep; false to remove
return $billboard->isDisplayable();
});
If you want to use this logic on a query (before the Collection is even built), you can create a query scope on your Billboard model:
public function scopeDisplayable($query, $displayable = true)
{
if ($displayable) {
// modify $query to only get displayable items, e.g:
$query->where('displayable', '=', 1);
} else {
// modify $query to only get non-displayable items, e.g:
$query->where('displayable', '=', 0);
}
}
// use your new query scope like any other method on the query builder:
// displayable billboards:
$billboards = Billboard::displayable()->get();
// non-displayable billboards:
$billboards = Billboard::displayable(false)->get();

In Laravel how to use snake case for database table columns and camel case for model attributes

I am new to laravel 4 and I am trying to create a rest API following best practices defined by Apigee.
One of the best practice defined by apigee is to use camel case for json attribute keys, this way when using the API in Javascript the corresponding objects will follow attributes code convention (camel case).
I want to be able to define datatable columns following snake case but when retrieving eloquent objects though my api, the corresponding JSON has to follow camel case.
I read about a static variable $snakeAttributes that could be set in the model class and its documentation says "Indicates whether attributes are snake cased on arrays". I tried to override this variable and set it to false (MyResource class) but when executing the folowing code, the json still comes in snake case:
Code:
$resource = MyResource::find($id);
return Response::json($resource);
JSON:
{
first_name: 'Michael',
created_at: "2013-10-24 15:30:01",
updated_at: "2013-10-24 15:30:01"
}
Does someone have an idea on how to solve that?
Create BaseModel and a new method to help you with it:
class BaseModel extends \Eloquent {
public function toArrayCamel()
{
$array = $this->toArray();
foreach($array as $key => $value)
{
$return[camel_case($key)] = $value;
}
return $return;
}
}
Your model:
class MyResource extends BaseModel {
}
And then use it:
$resource = MyResource::find($id);
return Response::json( $resource->toArrayCamel() );
The way I see, you'll have to make a array, work manually on the keys (camel case) and then convert the array (not the result) on a JSON.
$resource = MyResource::find($id);
$array = array();
foreach($resource as $key => $value) {
$key = str_replace('_', '-', $key);
$array[$key] = $value;
}
return Response::json($array);
I guess that will do the job. :D

Resources