Fillable list ignored while inserting related model - laravel-4

I am using Ardent and I faced strange behaviour of ignoring $fillable list while inserting/updating related models.
I have the following models defined:
class User extends LaravelBook\Ardent\Ardent
{
protected $table = 'users';
public static $relationsData = [
'contacts' => [self::HAS_MANY, 'Contact'],
];
}
class Contact extends LaravelBook\Ardent\Ardent
{
protected $table = 'user_contacts';
protected $guarded = ['*'];
protected $fillable = [
'user_id',
'type',
'value'
];
public static $relationsData = [
'user' => [self::BELONGS_TO, 'User'],
];
}
Now I am trying to add new contact to user:
$user->contacts()->create([
'type' => 'some type',
'value' => 'some value',
'unknown_field' => 'unknown value'
]);
... and I got SQL insert error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'unknown_field' in 'field list' (SQL: insert into `user_contacts` (`type`, `value`, `unknown_field`, `user_id`, `updated_at`, `created_at`) values (?, ?, ?, ?, ?, ?)) (Bindings: array ( 0 => 'some type', 1 => 'some value', 2 => 'unknown value', 3 => 2, 4 => '1384854899', 5 => '1384854899', ))
In the same time this is working fine:
UserContact::create([
'user_id' => 2,
'type' => 'some type',
'value' => 'some value',
'unknown_field' => 'unknown value'
]);
I didn't get any SQL errors and 'unknown_field' was just ignored.
Any ideas why $fillable fields could be ignored while working via builder?!

I don't understand why the HasManyOrOne relationship intentionally ignores fillable. It seems really counter intuitive. Either way, I think this should work for you.
$user->contacts()->save(Contact::create([ ... ]));

It seems I found the reason of this behaviour. This is explicitly implemented in HasOneOrMany abstract class.
abstract class HasOneOrMany extends Relation {
...
/**
* Create a new instance of the related model.
*
* #param array $attributes
* #return \Illuminate\Database\Eloquent\Model
*/
public function create(array $attributes)
{
$foreign = array(
$this->getPlainForeignKey() => $this->parent->getKey()
);
// Here we will set the raw attributes to avoid hitting the "fill" method so
// that we do not have to worry about a mass accessor rules blocking sets
// on the models. Otherwise, some of these attributes will not get set.
$instance = $this->related->newInstance();
$instance->setRawAttributes(array_merge($attributes, $foreign));
$instance->save();
return $instance;
}
...
}
I am still looking for the suffitient solution to control this behaviour.

As stated in the offical documentation:
To get started, set the fillable or guarded properties on your model.
You have set both. You should remove the following line: protected $guarded = ['*'];

Fortunately this will be fixed in version 4.2: https://github.com/laravel/framework/pull/2846
Added to all this, you can also filter the attributes manually:
$input = [
'user_id' => 2,
'type' => 'some type',
'value' => 'some value',
'unknown_field' => 'unknown value'
];
$fillable = $user->contacts()->getRelated()->fillableFromArray($input);
$user->contacts()->create($fillable);
Keeping in mind that the example are using Eloquent\Model\fillableFromArray() method, which is protected, so it will be necessary, for example, replicate it:
class BaseModel extends Eloquent
{
public function fillableFromArray(array $attributes)
{
return parent::fillableFromArray($attributes);
}
}

Use protected $guarded = array(); instead of protected $guarded = ['*'];
by using [*] you're telling laravel to guard all entities from autohydration / mass assignment!
array() sets this $guarded list to null.
The fillable property specifies which attributes should be mass-assignable. This can be set at the class or instance level.
The inverse of fillable is guarded, and serves as a "black-list" instead of a "white-list":
Read more at Laravel documentation on mass assignment

The update methods are not on the model level, and won't respect the $fillable fields.
You could filter the input data by using Input::only['fillable fields here']

Related

Laravel - Adding back slash to array after edit

this is my controller:
$sql = Raid::findOrFail($request['id']);
$sql = $sql->update($request->all());
I have a array in my table , after update value will be like this:
"{\"Plate\":0,\"Cloth\":0,\"Mail\":0,\"Leather\":0}"
but it should be:
{"Plate":"0","Cloth":"0","Mail":"0","Leather":"0"}
so I will get an error
before this , I was updating like this and it was ok:
$sql = Raid::where('id', $request['id'])->update($request->all());
and this is my model (traders and class_traders is fields that I have problem with):
use SoftDeletes;
use \OwenIt\Auditing\Auditable;
protected $table = 'raid';
protected $dates = ['date_and_time','deleted_at'];
protected $fillable = [
'admin_id', '....
];
protected $casts = [
'bosses' => 'array',
'traders' => 'array',
'class_traders' => 'array',
'boosters' => 'array',
];
I think what you need to do is don't update the model using the update($request->all()) way, but you need to specify each of the field's value. For the JSON field, you could try using json_encode() to save it.
That should do it.

Eloquent eager loading specific columns

I have two models :Product and category
which are linked by a one-to-many relationship. A category has several products. I would like to select specific columns from each model.
Here is the query I have, but I have all the columns with category_id, but I want the category name instead of id. How can I do that. Thank you in advance.
here is the method in controller
$products = Product::with('categories:id,name')->get();
if ($products) {
$response = ['api_status' => 1, 'api_message' => 'success', 'data' => $products];
return response()->json($response);
} else {
$response = ['api_status' => 0, 'api_message' => 'Error'];
return response()->json($response);
}
Here is category model
class Categorie extends Model
{
use HasFactory, SoftDeletes;
protected $fillable =['name','redirect'];
public function products()
{
return $this->hasMany(product::class);
}
}
and the product model is:
class Product extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'description',
'detail', 'img',
'categorie_id', 'onSale',
'costPrice', 'inStock', 'salePrice'
];
public function categories()
{
return $this->belongsTo(Categorie::class);
}
}
here is the response:
To modify the output of your model I'd suggest using an API resource. This will give you more granular control about how a resource is returned by the API. A resource is also the best point to modify certain values.
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'detail' => $this->detail,
'img' => $this->img,
'category_id' => $this->categorie->name,
'category_name' => $this->categorie->name,
'onSale' => $this->onSale,
'costPrice' => $this->costPrice,
'inStock' => $this->inStock,
'salePrice' => $this->salePrice,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'deleted_at' => $this->deleted_at,
'categories' => $this->categories ?? null,
];
}
}
This way you can manually specify which values your response should have.
In your controller you can include the populated array in your response by manually filling the toArray method with the current request object or just by using the resolve method which basically does the previous task for you:
$response = [
'api_status' => 1,
'api_message' => 'success',
'data' => ProductResource::collection($products)->resolve()
];
You can select particular fields from the relationship but you always need to select any keys involved in the relationship:
$products = Product::with('categories:id,name')->get();
Now each Product has its 'categories' loaded and those Category models only have the id and name fields.
Importantly:
The relationship categories is named incorrectly, it should be categorie in this case as the foreign key on Product is categorie_id and it is a singular relationship, it does not return multiple results.
Product::with('categorie:id,name')->get()
If you want to keep the name categories you would have to define the foreign key used when defining the belongsTorelationship, the second argument.
If you need to transform the structure of any of this that is a different thing and you will be walking into transformers or an API Resource.
Not sure how you want your data to look but this is the structure you will have by eager loading records, so if you need a different structure then what you get you will have to show an example.

Laravel - Mutator doesn't seem to work on update

I have the following mutator:
public function setFormattedCriteriaAttribute($value)
{
$this->attributes['formatted_criteria'] = serialize($value);
}
When I call the following why doesn't it update the formatted_criteria value - note the field is listed in my fillable attributes array ?
$jobAlert = JobAlert::findOrFail($id);
$jobAlert->update([
'frequency' => $request->frequency,
'criteria' => $criteria,
'formatted_criteria' => ['test']
]);
Be sure formated_criteria in your $fillable variable.
Update
if you have casts array in your model modify else add.
protected $casts = [
'formatted_criteria' => 'array',
];
then update your field as LONGTEXT with binary

Laravel Validation unique/exists with different database connection

In the documentation, I saw you could set a connection for the unique rule which is great. However, the exists doesn't seem to follow the same logic. Take this for example:
$rules = [
'username' => 'required|max:40|unique:user',
'name' => 'sometimes|required',
'email' => 'required|email|max:255|unique:int.user',
'password' => 'sometimes|required|confirmed|min:6',
'password_current' => 'sometimes|required'
];
The unique rule works GREAT in this instance. It uses my database connection called 'int' and calls the user table. HOWEVER, when the rules are reversed like so:
$rules['email'] = 'required|email|max:255|exists:int.user';
I got this error:
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'int.user'
doesn't exist (SQL: select count(*) as aggregate from int.user where
email = haleybuggs6#gmail.com)
It's trying to call an int.user table instead of using the int database connection.
Is there a reason exists doesn't act the same way as unique? Thanks.
instead of using connection name you can try with straight Database name which is defined in "int" connection. faced similar problem and these way worked for me. like
$rules['email'] = 'required|email|max:255|exists:DB_Name.user';
You can use
'email' => 'exists:mysql2.users|required'
Where mysql2 is second database settings array in the database.php file
Try it.
$rules = [
'username' => 'required|max:40|unique:connection_name.user',
'name' => 'sometimes|required',
'email' => 'required|email|max:255|unique:connection_name.user',
'password' => 'sometimes|required|confirmed|min:6',
'password_current' => 'sometimes|required'
];
Ultimately for Laravel 5.6.* you need to look at an existing instance of the model you are trying to validate, or specify ...
{db_connection_name}.{schema_name}.{table_name}
... to ensure that you are looking at the proper table.
Validation Example
validate it...
<?php
// for instance...
// maybe auth user is in a different db
// = so you cannot validate with your default db connection
$default_user = Auth::user();
// pass the instance in order to allow Validator to qualify the proper connection/name
\App\Validation\User::validate($_POST, $default_user);
User Validation class
<?php
namespace App\Validation;
class User extends Validator
{
/**
* #param \Illuminate\Database\Eloquent\Model|string $mixed
* #param string $default
* #return string
*/
public static function table($mixed,$default='default_connection.app_schema.users_table')
{
if($mixed instanceof \Illuminate\Database\Eloquent\Model){
$table = $mixed->getConnectionName().'.'.$mixed->getTable();
} else {
if (! empty($mixed)) {
$table = $mixed;
} else {
$table = $default;
}
}
return $table;
}
/**
* validation to create a new user
*
* #param array $data
* #param \App\User|string $mixed
* #return array
* #throws \Illuminate\Validation\ValidationException
*/
public static function validate(array $data, $mixed='default_connection.app_schema.users_table'){
return Validator::validate($data,[
'username' => 'required|max:40|unique:'.self::table($mixed),
'name' => 'sometimes|required',
'email' => 'required|email|max:255|unique:'.self::table($mixed),
'password' => 'sometimes|required|confirmed|min:6',
'password_current' => 'sometimes|required'
]);
}
}
$default_connection = 'db_name';
$rules = [
'username' => 'required|max:40|unique:{$default_connection}.user',
'name' => 'sometimes|required',
'email' => 'required|email|max:255|unique:int.user',
'password' => 'sometimes|required|confirmed|min:6',
'password_current' => 'sometimes|required'
];

Capitalized column name to retrieve data in Laravel 4

Overview:
I have this table called User
Notice that most of the column names are on StudlyCaps like every word has been capitalized.
Now, one of the problem that I've been experiencing is of course when logging in. It's mostly like Laravel doesn't like capitalized column names and such.
Here's my User Model I'll just put the relevant parts regarding on my problem.
class User extends Eloquent implements UserInterface, RemindableInterface {
protected $primaryKey = "UserID";
protected $fillable = array('Username', 'Password', 'Active');
protected $table = 'Users';
}
And here's my method where the user starts to log in.
public function postLogin() {
$validator = Validator::make(Input::all(),
array(
'username' => 'required',
'password' => 'required'
)
);
if ($validator->fails()) {
// Redirect
} else {
$auth = Auth::attempt(array(
'Username' => Input::get('username'),
'Password' => Input::get('password'),
'Active' => 1
));
if ($auth) {
return Redirect::intended('dashboard');
} else {
return Redirect::route('login')
->with('global', 'Username/Password wrong, or account not activated');
}
}
// Redirect
}
And here goes my error, it always say Username/Password wrong, or account not activated.
Any ideas on this one?
I think its best to rename all your columns 'snake_case' style. A) this will work nicely with Laravel and B) it's good practice to keep all your database table names and columns etc the same.

Resources