How to ignore uniqueness for multiple fields - php

I have two models: Cities and Schools. As you already understand Cities can have many schools and taking this into account I have defined my model as follows:
class School extends Model
{
public $fillable = ['city_id' ,'name'];
public function city()
{
return $this->belongsTo('App\City','city_id','id');
}
}
class City extends Model
{
public $fillable = ['name'];
Public function schools()
{
return $this->hasMany('App\School', 'id','city_id');
}
}
But I have faced a pproblem when trying to validate update of a school model. I have to validate whether name of the school is unique for selected city or not. I have defined the rule like this:
$rules = array(
'name' => ['required', Rule::unique('schools')->ignore($id)],
);
$validator=Validator::make(Input::all(),$rules);
But it is not allowing to save a school with existing name in other city than selected. How should I change the rule to ensure that school names can be same if the city is different.
Thank you.

Custom rule
The best solution would be to create a custom rule for this, that accepts the field with the corresponding city name/id as a parameter.
Something like
//Calling the custom rule like this
['name' => 'required|validateSchool:yourFieldNameOfCityHere'];
Declaring the custom validation function in your service provider like this
Validator::extend('validateSchool', function ($attribute, $value, $parameters, $validator) {
$cityName = ($validator->data, $parameters[0]);
//Now check if the $value is already set for the specific city by using normal database queries and comparison
return count(City::whereName($cityName)->schools()->whereName($value)->get()) == 0
});
What does it
The custom validation rule receives the data of the field you give with the function (in the code above it's yourFieldNameOfCityHere), so it knows which city the user chose. With this information, you now can check if there is already a school with the name for the entered city.

At the DB level, it sounds like what you want is a compound uniqueness constraint across name and city_id. Eloquent seems to support passing an array of column names in model definitions. It seems like this requires custom validation, though. See Laravel 4: making a combination of values/columns unique and the custom validator at https://github.com/felixkiss/uniquewith-validator

Related

I'm getting an error when trying to save a new model with 2 fields referencing different ids in the same table

I'm new to laravel, and I've picked up the basic workflow of creating, updating and deleting database entries using migrations, models and controllers. But now I'm trying to do the same with a subscriptions table that has a subscriberId and a followeeId in it. Both of these fields reference different ids of the same table (users). This kind of task seem to require some finetuning. And I'm stuck.
Here's my code with some comments.
Subscriptions Table
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('subscriberId');
$table->unsignedBigInteger('followeeId');
$table->foreign('subscriberId')->references('id')->on('users');
$table->foreign('followeeId')->references('id')->on('users');
});
Previously, I've used another approach to foreign ids, namely the one with the $table->foreignId('user_id')->constrained() pattern, but in this particular case I need to make sure that the two foreign ids reference different users, so I went for a more verbose option.
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
Here I've added the second parameter. This seems to work.
Subscription Model
class Subscription extends Model
{
use HasFactory;
protected $fillable = [
'subscriberId',
'followeeId'
];
public function subscriberId()
{
return $this->belongsTo(User::class, 'id', 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'id', 'followeeId');
}
}
Here I pass additional parameters, too, although in this case I'm not so sure if these are the correct ones. But this is my best guess. If I'm not mistaken, the second parameter of the belongsTo relation is inferred from the model that is being passed in, not the model of the parent class as is the case with the hasMany relation. So in this case that would be 'id' of the users table, which would be the default here anyway, but I need the third parameter, so I explicitly state the second parameter as well. Again, I'm not sure about this combination, but that's what I was able to make of the docs. I've also used other combinations of additional parameters, and even tried getting rid of these two public functions altogether, but that won't work either.
Now, here's the controller. If I do this:
$user->subscriptions()->get();
I do get the subscriptions I want. But if I do this instead:
$user->subscriptions()->create([
'subscriberId' => 1,
'followeeId' => 2
]);
I get the 500 error. I've also tried another approach:
$newSub = new Subscription;
$newSub->subscriberId = 1;
$newSub->followeeId = 2;
$newSub->save();
return $newSub;
But still no success. I still get the 500 error when I try to save()
Please help me out.
Solution
I should have used
public $timestamps = false
in the Subscription model, and I also misunderstood the docs. The correct combo is
User Model
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'subscriberId');
}
and
Subscription Model
public function subscriberId()
{
return $this->belongsTo(User::class, 'subscriberId');
}
public function followeeId()
{
return $this->belongsTo(User::class, 'followeeId');
}

how to require attaching related resources upon creation of resources - Laravel Nova

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'),

Yii2 - Ignore validation if

In my Yii2 project, I have related models. Example: A model Customer would have an attribute address_id that relates to another model Address. In the Customer model have the exist validator that checks that the row exists in the address table.
Typically, on create or update, this validation is ignored if address = null. My problem is that sometimes FE would send the address = 0 indicating the absence of an address.
In this case, I need to not only ignore validation, but to set address = null. This can be done beforeSave of course, but I'm trying to check if there's some built-in way through which I can do this
You can use the filter validator for normalization of an input data. For example:
class Customer extends ActiveRecord
{
public function rules()
{
return [
['address_id', 'filter', 'filter' => function ($value) {
return $value == 0 ? null : $value;
}],
// other validators
];
}
}

Foreign key query laravel - the eloquent way

I am trying to find a way to write eloquent way to query my model.
I have a Form model every form belongs to a User (Form:User = 1:1). Each User has a State and a City associated with them. Admin reviews a Form and each admin can be assigned to multiple State and City.
I want to find the Form that belongs to an Admin.
This is the forms function in Admin.php (Model)
public function forms()
{
//cities
$cities = $this->cities->pluck('name');
//states
$states = $this->states->pluck('name');
//get all form from the user and states
$forms = Form::whereHas('user',function ($query) use($cities,$states)
{
// find form from his states or cities
$query->whereIn('state',$states)->orWhereIn('city',$cities);
});
return $forms;
}
Currently it returns all the forms.
Any help will be appreciated!!!
You can try this:
$citiesForms =$this->cities->forms->toArray();
$statesForms = $this->states->forms->toArray();
return array_merge($citiesForms, $statesForms);
and you can look for hasManyThrough to make this done in one line

getting the value of an extra pivot table column laravel

I have a phone_models, phone_problems, and a phone_model_phone_problem pivot table. The pivot table has an extra column 'price'.
PhoneModel:
class PhoneModel extends \Eloquent
{
public function problems()
{
return $this->belongsToMany('RL\Phones\Entities\PhoneProblem')->withPivot('price');
}
}
PhoneProblem:
class PhoneProblem extends \Eloquent
{
public function models()
{
return $this->belongsToMany('PhoneModel')->withPivot('price');
}
}
What I'm trying to do is get the price of a specific phone with a specific problem.
This is how I have it now but I feel like Laravel has a built in Eloquent feature I can't find to do this in a much simpler way:
$model = $this->phoneService->getModelFromSlug($model_slug);
$problem = $this->phoneService->getProblemFromSlug($problem_slug);
all this does is select the specific model and problem from their slug.
then what I do is with those credentials I get the price like so:
$row = DB::table('phone_model_phone_problem')
->where('phone_model_id', '=', $model->id)
->where('phone_problem', '=', $problem->id)
->first();
so now I can get the price like so $row->price but I feel like there needs to be a much easier and more 'Laravel' way to do this.
When using Many to Many relationships with Eloquent, the resulting model automatically gets a pivot attribute assigned. Through that attribute you're able to access pivot table columns.
Although by default there are only the keys in the pivot object. To get your columns in there too, you need to specify them when defining the relationship:
return $this->belongsToMany('Role')->withPivot('foo', 'bar');
Official Docs
If you need more help the task of configuring the relationships with Eloquent, let me know.
Edit
To query the price do this
$model->problems()->where('phone_problem', $problem->id)->first()->pivot->price
To get data from pivot table:
$price = $model->problems()->findOrFail($problem->id, ['phone_problem'])->pivot->price;
Or if you have many records with different price:
$price = $model->problems()->where('phone_problem', $problem->id)->firstOrFail()->pivot->price;
In addition.
To update data in the pivot you can go NEW WAY:
$model->problems()->sync([$problemId => [ 'price' => $newPrice] ], false);
Where the 2nd param is set to false meaning that you don't detach all the other related models.
Or, go old way
$model->problems()->updateExistingPivot($problemId, ['price' => $newPrice]);
And remind you:
To delete:
$model->problems()->detach($problemId);
To create new:
$model->problems()->attach($problemId, ['price' => 22]);
It has been tested and proved working in Laravel 5.1 Read more.
Laravel 5.8~
If you want to make a custom pivot model, you can do this:
Account.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Account extends Model
{
public function users()
{
return $this->belongsToMany(User::class)
->using(AccountUserPivot::class)
->withPivot(
'status',
'status_updated_at',
'status_updated_by',
'role'
);
}
}
AccountUserPivot.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class AccountUserPivot extends Pivot
{
protected $appends = [
'status_updated_by_nice',
];
public function getStatusUpdatedByNiceAttribute()
{
$user = User::find($this->status_updated_by);
if (!$user) return 'n/a';
return $user->name;
}
}
In the above example, Account is your normal model, and you have $account->users which has the account_user join table with standard columns account_id and user_id.
If you make a custom pivot model, you can add attributes and mutators onto the relationship's columns. In the above example, once you make the AccountUserPivot model, you instruct your Account model to use it via ->using(AccountUserPivot::class).
Then you can access everything shown in the other answers here, but you can also access the example attribute via $account->user[0]->pivot->status_updated_by_nice (assuming that status_updated_by is a foreign key to an ID in the users table).
For more docs, see https://laravel.com/docs/5.8/eloquent-relationships (and I recommend press CTRL+F and search for "pivot")

Categories