I have 2 model in laravel 5.5
Article.php class with 2 function:
public function sluggable()
{
return [
'slug' => [
'source' => 'title'
]
];
}
public function path()
{
return "/$this->slug";
}
Category.php
public function childs() {
return $this->hasMany('App\Category','parent_id','id') ;
}
categories table:
id / article_id / parent_id / name
Now for example,for this code in view: {{ $article->path() }}
it prints: `example.com/article_slug`
But I want simething like this:
example.com/parentCategory/subCategory-1/.../subCategory-n/article_slug
How can I do it in path() function? Is it possible?
I'm assuming your question is asking how you can generate uri's for categories that can have an unlimited amount of children via their slugs.
How I would go about tackling something like this is by using a hierarchical data pattern within MySQL which will allow you to get a list of descendants / ancestors by performing one query. There are numerous ways to implement this, but for the purpose of this explanation I'm going to explain how to do it using the nested set pattern. More specifically, I'll be giving demonstrating how to do this using lazychaser's nested set package.
Categories Table Migration
use Kalnoy\Nestedset\NestedSet;
Schema::create('categories', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
NestedSet::columns($table);
$table->timestamps();
});
The nested set columns will add _lft, _rgt, and parent_id columns to your table. I would recommend you do some research on how the nested set model works in order to understand what the left and right columns are used for.
Category Model
use Kalnoy\Nestedset\NodeTrait;
class Category extends Model
{
use NodeTrait;
//
}
Now you can create a child category like so:
$parentCategory = Category::first();
$parentCategory->children()->create([
'name' => 'Example Category'
]);
This means that on a deeply nested category you can do:
$categories = Category::ancestorsAndSelf($article->category_id);
This will return all ancestors of the above category, then to get the uri you can do something like:
$uri = $categories->pluck('slug')->implode('/') . '/' . $article->slug;
You need to use the SluggableTrait at the top of your model like so :
use Cviebrock\EloquentSluggable\Sluggable;
class Post extends Model
{
use Sluggable;
/**
* Return the sluggable configuration array for this model.
*
* #return array
*/
public function sluggable()
{
return [
'slug' => [
'source' => 'title'
]
];
}
}
Related
I have a model called Tree that is supposed to be associated to 1..n Things. Things can be associated to 0..n things. In other words this is a many-to-many relationship, and a Thing must be chosen when a Tree is being created. My thing_tree migration looks like this (there's also a thing_thing pivot table but that's irrelevant):
public function up()
{
Schema::create('thing_tree', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->unsignedBigInteger('tree_id')->nullable();
$table->unsignedBigInteger('thing_id')->nullable();
$table->unique(['tree_id', 'thing_id']);
$table->foreign('tree_id')->references('id')->on('trees')->onDelete('cascade');
$table->foreign('thing_id')->references('id')->on('things')->onDelete('cascade');
});
}
My Tree model looks like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Tree extends Model
{
use HasFactory;
protected $guarded = [];
public function path(){
$path = '/trees/' . $this->id;
return $path;
}
public function associatedThings () {
return $this->belongsToMany(Thing::class);
}
}
The Thing model looks like this:
public function trees()
{
return $this->belongsToMany(Tree::class);
}
public function parentOf (){
return $this->belongsToMany(Thing::class, 'thing_thing', 'parent_id', 'child_id');
}
public function childOf(){
return $this->belongsToMany(Thing::class, 'thing_thing', 'child_id', 'parent_id');
}
Finally, the Tree Nova resource has these fields:
public function fields(Request $request)
{
return [
ID::make(__('ID'), 'id')->sortable(),
Text::make('name'),
ID::make('user_id')->hideWhenUpdating()->hideWhenCreating(),
Boolean::make('public'),
BelongsToMany::make('Things', 'associatedThings')
];
}
It should not be possible to create a Tree without an attached Thing, but the creation screen looks like this:
How do I require this in Nova?
This is not possible through nova's default features. Here is how I would go about it with the least effort (you Might want to create a custom field for that yourself) - or at least how I solved a similar issue in the past:
1. Add the nova checkboxes field to your project
2. Add the field to your nova ressource :
// create an array( id => name) of things
$options = Things::all()->groupBy('id')->map(fn($e) => $e->name)->toArray();
// ...
// add checkboxes to your $fields
Checkboxes::make('Things', 'things_checkboxes')->options($options)
3. Add a validator that requires the things_checkboxes to be not empty
4. Add an observer php artisan make:observer CheckboxObserver that will sync the model's relations with the given id-array through the checkboxes and then remove the checkboxes field from the object (as it will throw a column not found otherwise), so something like this:
public function saving($tree)
{
// Note: In my case I would use the checkbox_relations method of the HasCheckboxes trait and loop over all checkbox relations to perform the following and get the respective array keys and relation names
$available_ids = array_unique($tree['things_checkboxes']);
// Attach new ones, remove old ones (Relation name in my case comes from the HasCheckboxes Trait)
$tree->things->sync($available_ids);
// Unset Checkboxes as the Key doesn't exist as column in the Table
unset($tree['things_checkboxes']);
return true;
}
5. Add the same thing in reverse for the retreived method in your observer if you want to keep using the checkboxes to handle relations. Otherwise, add ->hideWhenUpdating() to your checkbox field
I added a trait for that to easily attach the relations through checkboxes to a model:
trait HasCheckboxRelations
{
/**
* Boot the trait
*
* #return void
*/
public static function bootHasCheckboxRelations()
{
static::observe(CheckboxObserver::class);
}
/**
* Defines which relations should be display as checkboxes instead of
* #return CheckboxRelation[]
*/
public static function checkbox_relations()
{
return [];
}
}
And checkbox_relations holds an array of instances of class CheckboxRelation which again holds informations about the key name, the relation name and so on.
public function __construct(string $relationName, string $relatedClass, string $fieldName, bool $hasOverrides = false, string $relationType = null, array $_fields = [])
Also, I added a method attachCheckboxRelationFields to the default nova resource which will be called on the $fields when the model uses the trait.
Now, I only have to add HasCheckboxRelations to a model, add the array of checkbox_relations and thats it - I have a belongsToMany relation on the nova resource through checkboxes. Of course you don't have the option to manage pivot fields anymore if you go for it this way - which might be why it was not done by the nova devs - but for simple belongsToMany relations I really like to work with the checkbox solution instead of the default attach-table. And for data with pivot fields you can still use the default way.
Also note that parts of the code where written on the fly so it might not work out of the box, but the overall idea should be delivered.
Hope it helped!
alternative
https://github.com/Benjacho/belongs-to-many-field-nova
BelongsToManyField::make('Role Label', 'roles', 'App\Nova\Role'),
I want to change the naming convention of the following url :
http://example.org/designs/CV%20Designs
To be the following:
http://example.org/designs/cv-designs
Here is my web.php route file:
Route::get('designs/{design}', 'DesignsController#show')
->name('designs.show');
And here is my Design Model:
public function getRouteKeyName()
{
$slug = Str::slug('Laravel 5 Framework', '-');
dd($slug);
return 'designName';
}
When I dd(slug); the output is 'Laravel 5 Framework' but I want it to be designName
Ok, so I am going to make some assumptions here, but lets say you have this function on your Design model:
Design.php
class Design extends Model
{
...
/**
* Assuming you dont have a slug column on your designs table
*
* Also assuming the slug is built from a column called 'name' on
* your designs table
*/
public function getSlugAttribute()
{
return \Illuminate\Support\Str::slug($this->name);
}
// This assumes there is a column on posts table of 'design_id'
public function posts()
return $this->hasMany(Post::class);
}
...
}
Now let's make an example of how you can build the desired route.
EDIT
Through further discussions with the asker, they wan't to show all Posts related to the Design they are showing (See model above). The setup in this answer is suitable for that, and you can refer to the show method defined below.
Assume we have DesignsController.php:
class DesignsController extends Controller
{
...
public function index()
{
return view('designs.index', [
'designs' => Design::all(),
]);
}
public function show(Request $request, string $design)
{
// It is worth noting here if name has a double space you wont
// be able to build backwards to it for a query
// ie) Design\s\sOne !== $lower(Design\sOne)\
$spaced = str_replace('-', ' ', $design);
$lower = strtolower($spaced);
$design = Design::with('posts')->whereRaw("LOWER(name) = '$lower'")->first();
return view('designs.show', [
'design' => $design,
]);
}
...
}
Now in the 'designs/index.blade.php' file you could do something like:
#foreach($designs as $design)
{{ $design->name }}
#endforeach
That would list all of your designs by name, linked to the designs.show route by their slug.
If you would always like the slug value to be loaded when serializing to an array or json, you can add it to the protected $appends array on the model.
If you don't ALWAYS want it appended, you need to append it at run time using for example $design->append('slug').
Or if you have a collection of Designs you can do $designs->each->append('slug').
Now in your designs.show blade file you can, access the Design's posts using the relation we loaded using Design::with('posts'), by doing the following:
#foreach ($design->posts as $post)
<img src="{{ asset('storage/'.$post->postImage) }}">
#endforeach
I'm trying to get pretty links with Laravel.
I have posts with tags. My tagged posts are viewable via localhost/tag-slug. I store tags slugs in database.
I'm trying to append and prepend these slugs with some words so I can have something like:
localhost/awesome-laravel-posts
localhost/best-php-posts
I can get it to work with:
Route::get('/awesome-{tag}-posts', 'TagsController#index')->name('tag')
But when the tag slug contains a -, it fails.
Here is my model:
class Tag extends Model
{
public function jobs()
{
return $this->belongsToMany(Post::class);
}
public function getRouteKeyName()
{
return 'slug';
}
}
Any ideas how can I achieve this? Is it even possible?
Of course u can do tha. I recommend dedicated library for example Eloquent Sluggable: https://github.com/cviebrock/eloquent-sluggable
Installation:
composer require cviebrock/eloquent-sluggable:^4.3
Usage:
use Cviebrock\EloquentSluggable\Sluggable;
class Post extends Model
{
use Sluggable;
/**
* Return the sluggable configuration array for this model.
*
* #return array
*/
public function sluggable()
{
return [
'slug' => [
'source' => 'title'
]
];
}
}
Easy creation of slugs for your Eloquent models in Laravel.
You can use regex to define route arguments. You could do this and {tag} will match anything:
Route::get('/awesome-{tag}-posts', 'TagsController#index')
->where('tag', '.*')->name('tag');
You could probably write a better regex for your case.
I am new to yii2. I have read the documentation and some answers on sof but still I cant get to work with relations in yii2. I am able to create raw mysql query for the problem but I dont know how to create the same query using yii2 relations. I am confused with via, joinWith and some key concepts. I will make the problem as descriptive as possible.
I have four models.
Category, CategoryNews, NewsTags, Tags
category table - cat_id, cat_name
news_category table - nc_id, nc_cat_id, nc_news_id
news_tags table - nt_id, nt_news_id, nt_tag_id
tags table - tag_id, tag_name
What I need is tags model object for each category, that is for each category i need all news tags belonging to that category. Request is from gridview.
The generated relations are:
Category Model:
public function getNewsCategory()
{
return $this->hasMany(NewsCategory::className(), ['nc_cat_id' => 'cat_id']);
}
NewsCategory Model:
public function getNcNews()
{
return $this->hasOne(News::className(), ['news_id' => 'nc_news_id']);
}
public function getNcCat()
{
return $this->hasOne(Category::className(), ['cat_id' => 'nc_cat_id']);
}
NewsTags Model:
public function getNtNews()
{
return $this->hasOne(News::className(), ['news_id' => 'nt_news_id']);
}
public function getNtTag()
{
return $this->hasOne(Tags::className(), ['tag_id' => 'nt_tag_id']);
}
News Model:
public function getNewsCategory()
{
return $this->hasMany(NewsCategory::className(), ['nc_news_id' => 'news_id']);
}
public function getNewsTags()
{
return $this->hasMany(NewsTags::className(), ['nt_news_id' => 'news_id']);
}
Tags Model:
public function getNewsTags()
{
return $this->hasMany(NewsTags::className(), ['nt_tag_id' => 'tag_id']);
}
ie. each category contains multiple news and each news contain mutiple tags and I need all tags related to each category.
More precisely, on the gridview I need all categories and a column displaying all tags related to these categories.
Please help!!
You can avoid declaration of models for junction tables, using viaTable syntax for many-to-many relations. Then your code will contain only three models (Category, News and Tag) and everything will be much simplier.
Your code for AR models and relations could looks as follows:
public class Category extends ActiveRecord
{
public function getNews()
{
return $this->hasMany(News::className(), ['id' => 'news_id'])
->viaTable('news_category_table', ['category_id' => 'id']);
}
}
public class News extends ActiveRecord
{
public function getCategories()
{
return $this->hasMany(Category::className(), ['id' => 'category_id'])
->viaTable('news_category_table', ['news_id' => 'id']);
}
public function getTags()
{
return $this->hasMany(Tags::className(), ['id' => 'tag_id'])
->viaTable('news_tags_table', ['news_id' => 'id']);
}
}
public class Tag extends ActiveRecord
{
public function getNews()
{
return $this->hasMany(News::className(), ['id' => 'news_id'])
->viaTable('news_tags_table', ['tag_id' => 'id']);
}
}
These relations you can use in link and unlink functions (rows in junction tables will be managed by Yii in backround). But keep in mind that you should use TRUE as second param in unlink() to remove row in junction table:
$article = new News();
$tag = new Tag();
$tag->save();
$article->link('tags', $tag);
$article->link('caterories', $category);
OR vice versa
$tag->link('news', $article);
$category->link('news', $article);
To get all tags in given category you can declare following function in Category class:
public function getTags()
{
return Tags::find()
->joinWith(['news', 'news.categories C'])
->where(['C.id' => $this->id])
->distinct();
}
This will work as relation query and you can use it as $category->tags or as $category->getTags()->count() or any other way (but not in link and unlink functions).
P.S. To use provided example in your code You should first change names, because I used singular form for AR classes names (Tag) and short notation for primary and foreign keys (id, tag_id etc). And I'd recommend you also to use such naming approach in your code and DB structure.
P.P.S. This example code wasn't tested so be careful :)
Simplifing, I've two tables:
Product: id, name
Datasheet: id, product_id
Where product_id points to products.id. Each Product could have 0 or 1 Datasheet.
Into my Product class (wich extends ActiveQuery) I've created this relation
/**
* #return \yii\db\ActiveQuery
*/
public function getDatasheet()
{
return $this->hasOne(Datasheet::className(), ['product_id' => 'id']);
}
I'm able now to query in this way
$products_without_datasheet = Product::find()
->with('datasheet')
->all();
What I really need is to retrieve only the products without the datasheet.
I'd like to create a 'scope' (like in yii 1) to be able to reuse the resulting condition datasheet.id IS NULL because this situation has a lot of variants and will be used all around the app.
I'm not able to understand how to create a relation with an added filter, something like getWithoutDatasheet() to be used as
Product::find()->with('withoutDatasheet')->all();
or
Product::find()->withoutDatasheet()->all();
Is it possible? And how?
You need create ActiveQuery for Product. See how gii generated ActiveRecord with ActiveQuery.
In Product:
/**
* #inheritdoc
* #return ProductQuery the active query used by this AR class.
*/
public static function find()
{
return new ProductQuery(get_called_class());
}
In ProductQuery:
public function withoutDatasheet()
{
$this->with('datasheet');
return $this;
}
Usage:
Product::find()->withoutDatasheet()->all();
To retrieve only the products without the datasheet you can do it like this:
$productsWithDatasheet = Datasheet::find()
->select('product_id')
->distinct()
->asArray()
->all();
$productIdsWithDatasheet = ArrayHelper::getColumn($productsWithDatasheet, 'product_id');
$productsWithoutDatasheet = Product::find()
->where(['not in', 'id', $productIdsWithDatasheet ])
->all();