Laravel validation: unique with multiple columns and soft_delete - validation

I am trying to do a Laravel validation rules as follow:
"permalink" => "required|unique:posts,permalink,hotel_id,deleted_at,NULL|alpha_dash|max:255",
The explanation to the rules is:
I have a table "Posts" in my system with the following fields (among others): hotel_id, permalink, deleted_at. If MySQL would allow make an unique index with null values, the sql would be:
ALTER TABLE `posts`
ADD UNIQUE `unique_index`(`hotel_id`, `permalink`, `deleted_at`);
So: I just add a new row IF: the combination of hotel_id, permalink and deleted_atfield (witch must be NULL) are unique.
If there is already a row where the permalink and hotel_id field are the same and 'deleted_at' field is NULL, the validation would return FALSE and the row wouldnt be inserted in the database.
Well. I don't know why, but the query Laravel is building looks like:
SELECT count(*) AS AGGREGATE FROM `posts`
WHERE `hotel_id` = the-permalink-value AND `NULL` <> deleted_at)
What the heck...
The query I was hoping Laravel build to validation is:
SELECT count(*) AS AGGREGATE FROM `posts`
WHERE `permalink` = 'the-permalink-value' AND `hotel_id` = ? AND `deleted_at` IS NULL
Could someone explain me how this effectively works? Because everywhere I look it looks like this:
$rules = array(
'field_to_validate' =>
'unique:table_name,field,anotherField,aFieldDifferentThanNull,NULL',
);
Does anyone could help me?
Thank you

all.
Finally, I got a proper understanding of the validation (at least, I think so), and I have a solution that, if it is not beautiful, it can helps someone.
My problem, as I said before, was validate if a certain column (permalink) is unique ONLY IF other columns values had some specific values. The problem is the way Laravel validation string rules works. Lets get to it:
First I wrote this:
"permalink" => "required|unique:posts,permalink,hotel_id,deleted_at,NULL|alpha_dash|max:255",
And it was generating bad queries. Now look at this:
'column_to_validate' => 'unique:table_name,column_to_validate,id_to_ignore,other_column,value,other_column_2,value_2,other_column_N,value_N',
So. The unique string has 3 parameters at first:
1) The table name of the validation
2) The name of the column to validate the unique value
3) The ID of the column you want to avoid (in case you are editing a row, not creating a new one).
After this point, all you have to do is put the other columns in sequence like "key,value" to use in your unique rule.
Oh, easy, an? Not so quickly, paw. If you're using a STATIC array, how the heck you will get your "currently" ID to avoid? Because $rules array in Laravel Model is a static array. So, I had to came up with this:
public static function getPermalinkValidationStr() {
$all = Input::all();
# If you are just building the frozenNode page, just a simple validation string to the permalink field:
if(!array_key_exists('hotel', $all)) {
return 'required|alpha_dash|max:255';
}
/* Now the game got real: are you saving a new record or editing a field?
If it is new, use 'NULL', otherwise, use the current id to edit a row.
*/
$hasId = isset($all['id']) ? $all['id'] : 'NULL';
# Also, check if the new record with the same permalink belongs to the same hotel and the 'deleted_at' field is NULL:
$result = 'required|alpha_dash|max:255|unique:posts,permalink,' . $hasId . ',id,hotel_id,' . $all['hotel'] . ',deleted_at,NULL';
return $result;
}
And, in the FrozenNode rules configuration:
'rules' => array(
'hotel_id' => 'required',
'permalink' => Post::getPermalinkValidationStr()
),
Well. I dont know if there is a easiest way of doing this (or a much better approach). If you know something wrong on this solution, please, make a comment, I will be glad to hear a better solution. I already tried Ardent and Observer but I had some problems with FrozenNode Administrator.
Thank you.

Related

Laravel Eloquent updateOrCreate doesn't work properly

I once wrote probably same question last time and I'm back..
Laravel Eloquent firstOrCreate doesn't work properly
On the last question, I found that fillable property filters update field manifest. So, if you want to update a table based on fieldA and fieldB, then your code might be..
$modelOrRelation->updateOrCreate(
['fieldA' => 'a', 'fieldB' => 'b'], ['otherfields' => 'update value']
);
and you MUST specify those fields on fillable property. $fillable = ['fieldA', 'fieldB', ...]
This is what I know about firstOrCreate and updateOrCreate.
At this time, following code generate many same rows. It looks like, the first parameter ['candle_date_time_kst'] do nothing..
// candleRelation is hasMany relation..
$candleRelation = $market->candles($period);
$created = $created->add($candleRelation->updateOrCreate(
[
'candle_date_time_kst' => $time,
],
$item
));
This creates many same candle_date_time_kst value rows. At this time, fillable property already filled target fields.
What else do I miss?
Is updateOrCreate should not trust? I didn't think so.. There are something I miss... any insight?
#220114 update
So, I do my homework..
Using DB::getQueryLog(), I get this query..
It looks like, updateOrCreate() remembers the last update value. Then if I reuse same eloquent relation object for another updateOrCreate(), method use last update parameter again. It makes and clause, so return record is none..
So, I use newQuery() method for initialize query bindings.
$created->add($candleRelation->newQuery()->updateOrCreate(
[
'candle_date_time_kst' => $time
],
$item
));
#220114
Unfortunately, retest reveals newQuery() actually not helping..
I tried $relation->newModelInstance() and getting same bindings.
What I trying to do is getting same parent binding without anything else. .. anyone knows?
Based on binding, when I get relation model I can get clean binding also. So I just do below..
$created->add($market->candles($period)->updateOrCreate(
[
'candle_date_time_kst' => $item['candle_date_time_kst']
],
$item
));
Only change is $candleRelation to $market->candles($period).
On each attempt, new relation instance produce so binding problem won't even exists.
.... I'm mad.
you need to supply an array in the format
[ column => value, ... ] not [ value ]
I had a similar problem a time ago. And the UpdateOrInsert method solved it.
Unfortunately, this method is Query Builder, not eloquent. But to achieve this result that was the only really working solution to me.
The issue for only happened when I tried to use more than 1 column on where clause, like in your example.

I get duplicate entries with the attach method in Laravel 5. Not sure what the problem is

So I'm trying to attach id's with some meta data to a pivot table in Laravel 5.
For some reason, I get the two inserts where there should be one, and the wrong ID's being inserted the second time round.
I'm not sure if there is something I might be missing here.
This is the code:
$match_values = array(
'dataId' => $result->id,
'dataMetaId' => $the_meta->id
);
$result->campaignDataMeta()->attach($match_values, [
'meta_value' => $value
]);
The database structure consists of a main campaignData table for email campaigns, a campaignDataMeta table (id, timestamps, name) for email meta data names, and a lookup table campaignDataMatches (id, campaignDataId, campaignDataMetaId, meta_value).
In campaignDataMatches I get the campaignDataId value sometimes being inserted into the campaignDataMeta column.
I've solved the problem.
Apparently had to add the relevant ID (in this case the dataMetaId) within the attach parameter.
Like this:
$result->dataMeta()->attach([$data_meta_id => [
'meta_value' => $value
]]);
Check the database columns primary maybe the dataId and metaId are both primary.

cakebake not working with prefix in cakephp3

I am using cakephp 3.4.9. When I am using a table with prefix n field its working properly after baking but if I use prefix in table fields its not working.
Like when I am using post with following fields like
id,
post,
date
it's working fine but if I use following fields its not working
p_id,
p_post,
p_date
it is adding extra codes in model
$this->belongsTo('Ps', [
'foreignKey' => 'p_id',
'joinType' => 'INNER'
]);
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['p_id'], 'Ps'));
return $rules;
}
why ps is adding here? If I use articales table like same its become As.
Please help.
I would like to suggest you, read this article.
CakePHP naming convention documentation
In cakePHP framework everything you have to keep in mind while creating the table is the CakePHP naming conventions. In your case, This is happening because cakePHP expects the primary column of any table will be only 'id', and the foreign key for the table will be the Related table name with an underscore id
(ex: If product table BelogsTO categories you have to make a column in your product table as category_id)
In your case cakePHP considering p_id as a foreign key for the table P. And by default cakePHP has a validation for the forein key that the existsIn which means that while saving that p_id, it will check for the existance of id in P table.
In one sentense this is because of the naming convention issue. You can change only p_id to id and keeping other things same will work for you.
HAPPY CODING :)

Cake php 2: Sort on 2nd-level Association

I wrote a post last year about this. The first part, how to sort on a 1st-level Association, was answered (thanks again!) but the second part of the question, about how to sort on a 2nd-level Association, was never answered. It wasn't a big issue, and we ran out of time so I never actually implemented it. But now we're updating the web, and the client wants this functionality, namely, with the following Models:
Company belongsTo City
City belongsTo Country, hasMany Companies
Country hasMany Cities
In the Companies page I want to sort on City.Country.name. Even putting recursive=2, it doesn't work. Cake ignores my 'order' condition; in the generated SQL there simply is no 'order by' at all.
It works fine if I sort on sort on City.name, however.
Is there any way to do this? I've been scouring the docs and Stackoverflow. I've looked at virtual fields, custom queries.
One way that seemed to look promising was to use Model->query() in the CompaniesController:
$companies = $this->Company->query("SELECT * FROM companies com left join cities c on c.id = com.city_id left join countries c2 on c2.id = c.country_id order by c2.name");
But, is this the best/only way to go? And do I not have to now worry about overriding pagination? I don't mind that but I would still like to use "normal" built-in pagination elsewhere for Companies. Will that be possible?
Also, in the examples in the docs, it says to do something like Model->query('SELECT * FROM pictures AS Picture LIMIT 2') so the resulting Array will use the model name as the array key. But how can I do this with my complex query? Where would the "AS" go?
I was rather hoping I'd be able to avoid having to do it like this though. Is there a simpler way to do it?
EDIT
Hi, thanks for your help. By "pagination technique" you mean Cake's built-in pagination? Yes, that's what I want. My default paging conditions in the controller are:
$this->paginate = array('conditions' => 'order' => array('Company.name' => 'ASC');
And it sorts on company name. The SQL is
SELECT Company.id, etc. FROM companies AS Company LEFT JOIN cities AS City ON Company.city_id = City.id order by Company.name
And when I create paging links in the View like this
$paginator->sort('City.name')
it adds these parameters to the url
.../companies/sort:City.name/direction:desc
it sorts on City name. The SQL is
SELECT Company.id, etc. FROM companies AS Company LEFT JOIN cities AS City ON Company.city_id = City.id order by City.name
But when I try this:
$paginator->sort('City.Country.name');
it adds these parameters to the url
.../companies/sort:City.Country.name/direction:asc
and the generated SQL is
SELECT Company.id, etc. FROM companies LEFT JOIN cities AS City ON (Company.city_id = City.id)
It completely ignores the 'sort' condition, and there is no 'order by' at all. I'm not sure why. Maybe Cake just can't do this? I have 'recursive' set to 2.
The other option is Model->query, which I tried and got working, but I'd rather not use because I would have to override paginate and paginateCount methods, which isn't that hard, but the problem is that on the same page, and on other pages, I am already using 'normal' Cake paging for Companies. So if I override paginate and paginateCount, won't I have to change all of those pages to use the new, over-ridden paging? I'd like to avoid that, because it seems like overkill, and is working fine everywhere else, except for this one case.
Any assistance much appreciated.
Bob
Well, in the end I managed it, after perusing the docs and many examples online, like this:
First, in the Model, declare and implement custom findMethods.
class Company extends AppModel {
public $findMethods = array('companiesOrderByCountry' => true);
protected function _findCompaniesOrderByCountry($state, $query, $results = array()) {
if ($state === 'before') {
$query['joins'] = array(
array(
'table' => 'cities',
'alias' => 'City',
'type' => 'LEFT',
'conditions' => 'City.id = Company.city_id'
),
array(
'table' => 'countries',
'alias' => 'Country',
'type' => 'LEFT',
'conditions' => 'Country.id = City.country_id'
)
);
$query['order'] = array('Country.name' => 'asc');
return $query;
}
return $results;
}
Then in the Controller, conditionally call it, based on named params in request
if (!empty($this->request->params['named']['sort']) && $this->request->params['named']['sort'] == 'Country.name') {
// here we need to sort on Country.name. Call custom findMethod in Model
$this->paginate = array('companiesOrderByCountry','limit' => 10);
$companies = $this->paginate();
$this->set(compact('companies'));
} else {
// do a normal search
$this->paginate = array('limit' => 10,'conditions' => array('order' => array('Company.nombre' => 'ASC' ));
$companies = $this->paginate('Company');
$this->set('companies', $companies);
}
Lastly, create a link, passing the named parameter, in the .ctp
<?php $paginator = $this->Paginator; ?>
<?php echo $paginator->sort('Country.name', __('Country')); ?>
There's probably a more elegant way to do this, but I was sick of dealing with it. The generated sql is just what I need and now I can sort on Country, which is a 2nd-level (Company->City->Country) association.
Hope this helps someone someday!
Bob
If you want to try using the query method, then I would not use the asterisks (*). Instead explicitly list the fields you want.
Your query should look something like this: {My personal best practice, use COALESCE on fields that may be NULL - especially when using a LEFT JOIN}
SELECT companies.name AS CompanyName,
COALESCE(cities.name, 'Unknown') AS CityName,
COALESCE(countries.name, 'Unknown') AS CountryName
FROM companies
LEFT JOIN cities
ON companies.city_id = cities.id
LEFT JOIN countries
ON cities.country_id = countries.id
ORDER BY CountryName, CityName
Use AS to separate the field from its alias. It also separates a table from its alias ( e.g., the examples you listed.) AS is optional, but I feel it adds to readability.
--
(If you want to try the pagination technique, please show the MySQL SELECT statement that is failing when RECURSIVE = 2, along with what the controller's code looks like - specifically the 'order'=> array)

Laravel 4: making a combination of values/columns unique

I'm importing a bunch of csv entries in my database with Laravel 4.
I can't really point at one column that has to be unique, it's a combination of 5 columns that makes it unique. However: how does one define this in Laravel?
Option 1: schema builder
You can use the $table->unique('email') method, but that only seems to allow one column, not a combination of columns.
Option 2: Validation
Less preferable, but I could validate the model before inserting it. However, again, using 'unique:[table]' validation rules, it will return an error when just one of the column values isn't unique, not a combination of them.
Can anyone tell me how I should go about this?
I'm sure I'm missing something, but I could use a push in the right direction :-)
Thanks,
Dieter
You can combine:
$table->unique( array('email','name') );
And pretty much everything in Laravel will accept arrays to do whatever you need to with 'more than one'.
Use Schema Builder's unique() method to define your data model, as Antonio mentioned.
Additionally, if you want to use validation on your model, consider my custom Validator rule for multiple UNIQUE indexes: https://github.com/felixkiss/uniquewith-validator
You can also do this;
$table->unique(["column1", "column2"], 'uq_columns');
Which means that you will have a unique column combination of all the columns i.e. column1 and column2
I know this question is for Laravel 4, but I just came across this on searches and found a solution for Laravel >= 5.3
Here it is:
Of course, the migration may look something like
$table->unique( array('email','name') );
Then to validate this, you do not need to use custom rules, just advanced rules:
'email' => Rule::unique('users')->where(function ($query) use ($request) {
return $query->where('name', $request->name);
}),
Of course, you may want to validate name before of this. The name should be required so that you may finish with something like this:
'name' => 'required|max:255',
'email' => Rule::unique('users')->where(function ($query) use ($request) {
return $query->where('name', $request->name);
}),
I hope it helps.
You can try this
$table->string("name");
$table->string("email")->unique("name")

Resources