Adding custom eloquent attribute in Laravel 5 - laravel

I have a function named siblings which fetches all siblings of a user.
select siblings(id) as `siblings` from users where id = 1
I can access the function in Eloquent as
User::where('id', 1)->first([DB::raw(siblings(id) as `siblings`)]->siblings;
I want to make the siblings available via custom attribute.
I added siblings to $appends array
I also created getSiblingsAttribute method in my User model as
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('idd', $this->id)
->first([DB::raw('siblings(id) AS `siblings`')])
->siblings;
return explode(',', $siblings);
}
But this is not working as $this->id returns null
My table schema is users(id, username,...), so clearly id is present.
Is there a way by which I can bind the siblings function while querying db and then returning something like $this->siblings from getSiblingsAttribute. If I can bind siblings(id) as siblings with query select globally as we do for scopes using global scope.
That way my code can be simply
public function getSiblingsAttribute()
{
return $this->siblings;
}

The simplest way is to create a view in your database and use that as a table:
protected $table = 'user_view';
Otherwise I need more information about your id == null problem.
If you can fix this by your own in the next step it is important that you use an other column name by selecting as in your accessor otherwise you run in an infinite loop.
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('id', $this->id)
->first([DB::raw('siblings(id) AS `siblings_value`')])
->siblings_value;
return explode(',', $siblings);
}
EDIT
Sadly there is no simple way to archieve this.
But after a little bit tinkering I have found a (not very nice) solution.
Give it a try.
You have to add the following class and trait to your app.
app/Classes/AdditionalColumnsTrait.php (additional column trait)
namespace App\Classes;
trait AdditionalColumnsTrait {
public function newEloquentBuilder($query) {
$builder = new EloquentBuilder($query);
$builder->additionalColumns = $this->getAdditionalColumns();
return $builder;
}
protected function getAdditionalColumns() {
return [];
}
}
app/Classes/EloquentBuilder.php (extended EloquentBuilder)
namespace App\Classes;
use Illuminate\Database\Eloquent\Builder;
class EloquentBuilder extends Builder {
public $additionalColumns = [];
public function getModels($columns = ['*']) {
$oldColumns = is_null($this->query->columns) ? [] : $this->query->columns;
$withTablePrefix = $this->getModel()->getTable() . '.*';
if (in_array('*', $columns) && !in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_merge($columns, array_values($this->additionalColumns)));
} elseif (in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_values($this->additionalColumns));
} else {
foreach ($this->additionalColumns as $name => $additionalColumn) {
if (!is_string($name)) {
$name = $additionalColumn;
}
if (in_array($name, $columns)) {
if (($key = array_search($name, $columns)) !== false) {
unset($columns[$key]);
}
$this->query->addSelect($additionalColumn);
}
}
if (is_null($oldColumns)) {
$this->query->addSelect($columns);
}
}
return parent::getModels($columns);
}
}
after that you can edit your model like this:
class User extends Model {
...
use App\Classes\AdditionalColumnsTrait;
protected function getAdditionalColumns() {
return [
'siblings' => DB::raw(siblings(id) as siblings)),
];
}
...
}
now your siblings column will be selected by default.
Also you have the option to select only specific columns.
If you don't want to select the additional columns you can use: User::find(['users.*']).
Perhaps it is a solution for you.

Related

table relationship and how to use it in laravel controller

so, I have 2 tables, stage and event. Stage hasMany event, and Event belongsTo Stage. And I want to show all stage and its event as json. Here is my code in controller:
public function getschedule(){
$schedule = Stage::all();
//$event = Event_schedule2020::all();
if (!$schedule) {
return response()->json(['msg'=>'Error not found','code'=>'404']);
}
foreach($schedule->events as $array){
$datax[] = [
'id'=>$array->id,
'time'=>$array->time,
'category'=>$array->category,
'type'=>$array->title,
'designer'=>$array->designer,
];
}
foreach ($schedule as $item) {
$jadwal[] = [
'id'=>$item->id,
'date'=>$item->date,
'place'=>$item->stage,
'data'=>$datax,
];
}
return response()->json($jadwal);
}
but I always get this error
the error
so, is there anything I can do about this?
You can utilize inbuilt functions to do what you want to. Laravel automatically transforms model into JSON, no need to built arrays with it.
public function getschedule() {
// tell laravel you want to eager load events
$stages = Stage::with('events')->get();
// laravel knows you loaded events and therefor you can just return it and it does the rest automatically
return response()->json($stages);
}
in your Stage model you have to create relationship like this
public function events()
{
return $this->hasMany('App\Event');
}
then in your Controller
public function getschedule(){
$schedules = Stage::with('events')->get()->toArray();
return response()->json($schedules );
}
your mistake is call events on a collection for solve this you can change foreach like followings :
public function getschedule(){
$schedules = Stage::all(); // I add a 's' to $schedule because is better set plural name;
if (!$schedule) {
return response()->json(['msg'=>'Error not found','code'=>'404']);
}
foreach($schedules as $schedule){
$datax = [];
foreach($schedule->events as $event){
$datax[] = [
'id'=>$event->id,
'time'=>$event->time,
'category'=>$event->category,
'type'=>$event->title,
'designer'=>$event->designer,
];
}
$jadwal[] = [
'id'=>$item->id,
'date'=>$item->date,
'place'=>$item->stage,
'data'=>$datax,
];
}
return response()->json($jadwal);
}
but above solution is not recommended because send many request to server in any foreach loop, following solution is better:
public function getschedule(){
$schedules = Stage::with('events')->get(); // only this difference with above soloution and the rest is the same
if (!$schedule) {
return response()->json(['msg'=>'Error not found','code'=>'404']);
}
foreach($schedules as $schedule){
$datax = [];
foreach($schedule->events as $event){
$datax[] = [
'id'=>$event->id,
'time'=>$event->time,
'category'=>$event->category,
'type'=>$event->title,
'designer'=>$event->designer,
];
}
$jadwal[] = [
'id'=>$item->id,
'date'=>$item->date,
'place'=>$item->stage,
'data'=>$datax,
];
}
return response()->json($jadwal);
}

Laravel 5: directory-elements sort by name in a hasMany relationship

I need to Sort folders ($data['contents']) by relationship langs() where lang_id == 1 How can I do that? Thanks.
In Model Folder:
class Folder extends Model
{
protected $fillable = ['active', 'date', 'path', 'slug'];
public function langs()
{
return $this->hasMany('App\Models\LangFolder');
}
}
In Model LangFolder:
class LangFolder extends Model
{
protected $fillable = ['folder_id', 'lang_id', 'name', 'description'];
public function folder()
{
return $this->belongsTo('App\Models\Folder');
}
}
In Controller:
public function folders()
{
// Sort folders ($data['contents']) by relationship langs() where lang_id == 1
$data['folders'] = Folder::all();
$data['langs'] = Lang::all();
$directory = 'library/';
$elements = array_diff(scandir($directory), array('..', '.'));
foreach($elements as $element) {
if (is_dir($directory . '/' . $element)) {
$content_element = Folder::where('slug', $element)->first();
$data['contents'][] = $content_element;
}
}
return view('web.library.index', compact('data'));
}
If you want to order from a relation, you must use Collection instead of Eloquent, for example:
$orderedLangs = $folder->langs->sortBy('lang_id');
I have finally added the field "name" of the relationship to the array, and I have converted the array into a collection, and then, I have sorted by 'name' case insensitive. Maybe there is another cleaner solution. This works:
Controller:
foreach($elements as $element){
if(is_dir($directory.'/'.$element)){
$content_element = Folder::where('slug', $element)->first();
$contenidos[] = $content_element;
}else{
$data['documents'][] = Document::where('path', $directory.'/'.$element)->first();
}
}
foreach($contenidos as $contenido){
foreach($contenido->langs as $lang){
if($lang->lang_id == '1'){
$contenido->name = $lang->name;
}
}
$data['contents'][]= $contenido;
}
$data['contents']=collect($data['contents'])->sortBy('name', SORT_NATURAL|SORT_FLAG_CASE);

ManyToOne form creating linked object

Imagine these 2 entities:
Article
title
description
category <- ManyToOne
Category
name
Say we need to manage an article with a form and the category attached.
$builder
->add('name')
->add('description')
->add('category')
This one will allow me to select from existing categories.
Now I'd like to be able to create categories if needed.
Title [ ]
Description
[ ]
[ ]
Category [ ]
The category field would be a free text box.
If the text corresponds to no category, a new one would be created and attached.
I tried with some DataTransformer with no luck
I need a reusable solution to manage that because I'll need to embed it especially in another form as a collection.
How can I do it reusable ?
Suggest that you do an "if" on your category.
When you check your form submission is valid and submitted, get the 'category' data, and if not exists persist it as a new category then query for the article after.
The code might be something like this:
if ($form-isSubmitted() && $form->isValid()){
$catName = $form->get('category')->getData(); // Get category.
// Query if it exists.
$qb = $em->createQueryBuilder();
$qb->select('c')
->from('AppBundle:Category', 'c')
->where('c.name = :catName')
->setParameter('catName', $catName);
$cat_results = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
if ($cat_results == null){
$newCat = new Category();
$newCat()->setname($catName);
$em->persist($newCat);
$em->flush();
}
else{
...
\\ Render your form etc...
}
Hopefully you get the idea. The above code is simpler, because then you create the category in the same form.
Here is what I ended up with using a DataTransformer
My ArticleType:
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
->add('category', CategoryType::class)
//...
A new CategoryType class:
class CategoryType extends AbstractType
{
protected $myService;
public function __construct(MyService $myService)
{
$this->myService = $myService;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new CategoryTransformer($this->myService);
$builder->addModelTransformer($transformer);
}
}
Registered as service
app.form.category_type:
class: AppBundle\Form\CategoryType
arguments: ['#app.my.service']
tags:
- { name: form.type }
And finally the transformer:
class CategoryTransformer implements DataTransformerInterface
{
// ...MyService initialization...
public function transform($category)
{
if (null === $category) {
return '';
}
return $category->getName();
}
public function reverseTransform($categoryName)
{
if (!$categoryName) {
return null;
}
$category = $this->myService->getOrCreateCategoryFromName($categoryName);
if (null === $category) {
throw new TransformationFailedException();
}
return $category;
}
}
MyService is responsible to get or create a category with the given name using the entity manager.
Quite some lines of code but once this is done, wherever I'll use my form, it can be dealt in the easy standard way:
$articleForm = $this->createForm(ArticleType::class, $article);
$articleForm->handleRequest($request);
if ($articleForm->isValid()) {
$em->persist($article);
$em->flush();
//...

Retrieve parent class within morph relationship

I have this code
//ImageableTrait
trait ImageableTrait
{
public function images()
{
return $this->morphMany(Image::class, 'imageable')
->orderBy('order', 'ASC');
}
}
//User
class User extend Model
{
use ImageableTrait;
}
//Post
class Post extend Model
{
use ImageableTrait;
}
class ImageCollection extends Collection
{
public function firstOrDefault()
{
if ($this->count() === 0) {
$image = new Image();
$image->id = 'default';
$image->imageable_type = '/* I need the parent className here */';
$image->imageable_id = '.';
}
return $this->first();
}
}
//Image
class Image extend Model
{
public function imageable
{
return $this->morphTo();
}
public function newCollection(array $models = [])
{
return new ImageCollection($models);
}
public function path($size)
{
//Here, there is some logic to build the image path and it needs
//the imageable_type attribute no matter if there is
//an image record in the database or not
return;
}
}
I want to be able to do so
$path = User::find($id)->images->firstOrDefault()->path('large');
But I can't figure out how to get the parent class name to build the path properly...
I tried with $morphClass or getMorphClass() but can't figure out how to use it properly or if it is even the right way to do it.
Any thoughts on that?
I think you can keep it simple and drop the ImageCollection class because there is already a firstOrNew method that seems to be what you're looking for.
The firstOrNew method accepts an array of attributes that you want to match. If you don't care about the attributes, you can pass an empty array. If there are no matches in the database, it'll make a new instance with the proper parent type.
$path = User::find($id)->images()->firstOrNew([])->path('large');
Note: I am calling the images() method to get the MorphMany object so that I can call the firstOrNew method. In other words, you need to add the parentheses. Otherwise, you get a Collection.
Edit: If you want to make things a bit simpler by automatically setting some default attributes, you can add this to your ImageableTrait:
public function imagesOrDefault()
{
$defaultAttributes = ['id' => 'default'];
return $this->images()->firstOrNew($defaultAttributes);
}
Then, you can do something like this: $path = User::find($id)->imagesOrDefault()->path('large');
Note that your default attributes must be fillable for this to work. Also, imageable_id and imageable_type will automatically be set to your parent's id and type.
If you want to set the default value for imageable_id to a period and not the parent's id, then you have to alter it a bit, and it will look a lot like your original code except this will go inside your ImageableTrait.
public function imagesOrDefault()
{
// First only gets one image.
// If you want to get all images, then change it to get.
// But if you do that, change the conditional check to a count.
$image = $this->images()->first();
if (is_null($image))
{
$image = new Image();
$image->id = 'default';
$image->imageable_type = $this->getMorphClass();
$image->imageable_id = '.';
}
return $image;
}
Ok guys I've found something that seems to work pretty good for now so I'll stick with that.
In the Image model, I've added some code when I make the new collection:
public function newCollection(array $models = [])
{
$morphClass = '';
$parent = debug_backtrace(false, 2)[1];
if (isset($parent['function']) AND $parent['function'] === 'initRelation') {
if (isset($parent['args'][0][0])) {
$morphClass = get_class($parent['args'][0][0]);
}
}
return new ImageCollection($models, $morphClass);
}
I then simply retrieve the morphClass through the constructor of the ImageCollection
private $morphClass;
public function __construct($items = [], $morphClass)
{
parent::__construct($items);
$this->morphClass = $morphClass;
}
public function firstOrDefault()
{
if ($this->count() === 0) {
$image = new Image();
$image->id = 'default';
$image->imageable_type = $this->morphClass;
$image->imageable_id = '.';
}
return $this->first();
}
This way, I can simply call the method like that
User::with('images')->get()->images->firstOrDefault()
This seems to work really great in many cases, if I have some issues at some times, I'll update this post.
i may be late for the party, but i kinda did a small trick for morph relationships where i had "media" as morph, i get the parent since "model_type" has the full string parent class string.
$model = new $media->model_type;
$media->model = $model->findOrFail($media->model_id);

Yii2 relation with parameter

Is it possible and what would be the best way to define a relation with a parameter in Yii2.
Situation is simple. I have table texts and texts_regional. texts_regional of course has foreign keys text_id and lang_id.
Gii generated a method to get all regional texts but I dont need that on the frontend. I just need in the current language.
Generated method is:
public function getTextsRegionals()
{
return $this->hasMany(TextRegional::className(), ['text_id' => 'id']);
}
Tried this but it's probably not right:
public function getReg($langId=null)
{
if($langId === null && Yii::$app->session->has('langId')) {
$langId = Yii::$app->session->get('langId');
}
return $this->hasOne(TextRegional::className(), ['text_id' => 'id', 'lang_id'=>$langId]);
}
I need data from both tables so I'd like to eager load this.
Is it just better to use separate method and manually construct the query?
Read in documentation that it's possible to do ->onCondition so wrote a method like this:
public function getReg($langId=1)
{
if(Yii::$app->session->has('langId')) {
$langId = Yii::$app->session->get('langId');
}
return $this->hasOne(TextRegional::className(), ['text_id' => 'id'])->onCondition(['lang_id' => $langId]);
}
$langId is set in main controller.
But I ended up using TextRegional model and joined with Text model to set condition.
Made a TextRegionalQuery class and added a new method:
public function byCode($code)
{
if(Yii::$app->session->has('langId')) {
$langId = Yii::$app->session->get('langId');
} else {
$langId = 1;
}
$this->joinWith('text0')
->andWhere("lang_id = '".$langId."'")
->andWhere("texts.code = '".$code."'");
return $this;
}
Using it like this:
$ft = TextRegional::find()->byCode("footer_text")->one();
Or
$news = TextRegional::find()->byType(2)->visible()->all();
/**
* relation with current LangContractTemplate
*/
public function getCurLangContractTemplate()
{
if(isset(Yii::$app->user->identity->u_lang) && !empty(Yii::$app->user->identity->u_lang))
$langId = Yii::$app->user->identity->u_lang;
else
$langId = \Yii::$app->language;
return $this->hasOne(LangContractTemplate::className(), ['lcont_cont_id' => 'cont_id'])->onCondition(['lcont_lang_id' => $langId]);
}
//------------------OR------------------
/**
* relation with language table
*/
public function getContractByLang()
{
return $this->hasOne(LangContractTemplate::className(), ['lcont_cont_id' => 'cont_id']);
}
/* and Get data */
$contract_content = ContractTemplate::find()
->joinWith(['contractByLang' => function($query) use ($lang) {
return $query->where(['lcont_lang_id' => $lang]);
}])
->one();

Resources