SilverStripe unique URL (debug) - php

In this DataObject there is a user supplied field Title which has to be converted to a unique URL slug.
Desired Result: Duplicate URL's should get a suffix to its value. So saving 2 records with Title Foo should result in one record with foo as its value for column URL and the second record should have value foo-2 for the same column.
public function onBeforeWrite() {
parent::onBeforeWrite();
// Sanitize Title field to use for URL
$filter = URLSegmentFilter::create();
$this->URL = $filter->filter($this->Title);
// If URL is not unique, add suffix
$i = 1;
while($this->uniqueURL($this->URL)) {
$i++;
$this->URL = $this->URL . "-" . $i;
}
}
method: uniqueURL (within same class)
public function uniqueURL($URL) {
// Check if there is a record with the same URL
$existingURL = DataObject::get('NewsArticle', "URL = '$URL'");
if ($existingURL) {
// this is a duplicate URL
return false;
} else {
// this is a unique url
return true;
}
}
Saving Foo twice would result in foo and foo-2.
When saving two records with the same Title Foo results in two URL fields with foo

Why do you have two foo urls?
If you check your DB before inserting all records, this means that the check will not work on your record batch.
Don't use a loop to count unique urls
You don't need to loop and check every time and increment the count ($i). Performance wise youre far better off doing a COUNT() in a query and just use that value for your next insert.
// The following does exactly the same with just 1 query. No loop needed.
$count = DB::query("SELECT COUNT(*) FROM Table WHERE Title LIKE '{$filteredTitle}'")->value();
if ($count > 1) {
$filteredTitle .= "-" . $count;
}
$this->URL = $filteredTitle
Solutions
To do it onBeforeWrite() the only possibility is to Query your data AND check your records before they are saved.
Or a simpler solution with the same results is that you can change the url in an onAfterWrite() , and check use the amount of same titles as number.
public function onAfterWrite() {
parent::onAfterWrite();
// Sanitize Title field to use for URL
$filter = URLSegmentFilter::create();
$filteredTitle= $filter->filter($this->Title);
$count = DB::query("SELECT COUNT(*) FROM Table WHERE Title LIKE '{$filteredTitle}'")->value();
if ($count > 1) {
$filteredTitle .= "-" . $count;
}
$this->URL = $filteredTitle
}

Related

Comparison of data downloaded from the database

I have a question regarding php and sql. In the database I have a shop table 'x' in which there is a tags column containing mixed tags from which to filter the collection for the product using the operator 'like'. I have to compare the filtered collection with the collections in column 1 of the store '. If these two collections are equal, then from the store's table in column 2 I can retrieve the appropriate value and enter it into the csv file.
In php I have downloaded the column with tags from the 'x' store, but I have no idea how to add to the category that I searched for by query and compare it with the data from the store 's' column. I tried to explode the resulting strings from the foreach loop with explode () to get an array from each cell in the column, but got the primitive array that was fetched from the database. I also tried a regular for loop but it also failed.
How can you get this one value from the tags column and compare it with the data from another column ?
public static function findEmpikCategory()
{
$empik_lilante_categories = MysqlProvider::getEmpikCategory(); // empik and lilante categories from empik_categories in which the 'collection' is included
// get a specific category (lilante_category) from empik_categories
foreach($empik_lilante_categories as $categories)
{
$lilante_category_from_empik[] = $categories['lilante_category'];
}
return $lilante_category_from_empik;
}
//$lilante_category = explode(",", $lilante_category_for_offers_products, 0);
// searching lilante categories from the shopifyProductsLilante array
public static function findLilanteCategory()
{
$empik_cateries_equals_lilante_categories = MysqlProvider::getEmpikCategoryEqualLilanteCategory(); // empik categories corresponding to lilante categories
$lilante_category_to_check = CSVFileGenerator::findEmpikCategory();
$lilante_category_for_offers_products = MysqlProvider::getLilanteCategoryByOffersEmpik(); // lilante categories for all empik products
//print_r($lilante_category_to_check);
foreach($lilante_category_for_offers_products as $lilanteCategories)
{
foreach($lilanteCategories as $lilanteCategory)
{
echo "<br/>";
//print_r($lilanteCategory);
echo "<br/>";
print_r(explode(",", $lilanteCategory, 0));
//echo "<br/>";
// echo gettype($lilanteCategory) . "<br>"; // each element is a string
// print_r($lilanteCategory);
}
}
foreach($lilante_category_to_check as $lilanteCategoryFromEmpik)
{
echo "<br/>";
print_r($lilanteCategoryFromEmpik);
echo "<br/>";
}
//fclose($file_open);
return ;
}

PHP recursive creation of db queries after multidimensional form submission: fk field sometimes "omitted", but why and how to fix?

NOTE: SOLUTION FOUND
I am trying to build a (my first!) PHP/JQuery/MySQL web app able to work with multidimensional data. In current state, almost everything works fine but one strange bug occurs when submitting data (see title) and I haven't found any explanation. Can anyone open my eyes?
When working my test form with 5 dimensional data (table names - one, two, three, four, five - all joined in chain):
if I submit completely new entry with all dimensions then all 5 INSERT INTO queries are generated correctly
but if I add to the existing entry (under 2nd dim) new 3rd dimension with corresponding child data (4th and 5th) - that means tables three, four and five - then foreign key field in table five (four_id) is omitted from the insertion query
all the rest options (two, three, four and five or new four and five) have no issues
There are 3 functions in php that are doing the work (first for main table, second for next 2 dimensions and third (recursive) for next n dimensions). As tables four and five in this example are "belonging" to the recursive one I am quite sure that the issue and key for solution should be there.
Each function is using both form data and existing data that is already submitted. Pk value of parent row is passed to the child in two possible ways:
After each INSERT INTO query a MySQL variable for new pk value is created: SET #last_id_tablename = LAST_INSERT_ID() to be used in child query when needed. If data submitted to parent and child (say four and five) is new for both tables then child table's query should be (and normally is)
INSERT INTO five (four_id, title) VALUES (#last_id_four, 'Some text')
If parent data is already existing and we add new related child row then the existing parent pk value (say 1) is passed to the child and query is
INSERT INTO five (four_id, title) VALUES (1, 'Some text')
So the issue is that when I have an entry with first 2 dimensions and I add 3 dimensions under existing 2nd (IOW I have parent row in one with its child row in two and under this I add new data starting from table three the generated queries are:
INSERT INTO three (two_id, title) values (1, 'Some text');
INSERT INTO four (three_id, title) values (#last_id_three, 'Some text')
INSERT INTO five (title) values ('Some text')
As you see, four_id and #last_id_four are missing in third line.
All other combinations including fully new data submmission for all dimensions are generating a correct query for five. Fully new data submission query list looks like this one (first table's last id is returned before the rest continues, passed to the next function and therefore it's in use already as a real number, let's say 10)
INSERT INTO one (title) values ('Some text');
INSERT INTO two (one_id, title) values (10, 'Some text');
SET #last_id_two = LAST_INSERT_ID();
INSERT INTO three (two_id, title) values (#last_id_two, 'Some text');
SET #last_id_three = LAST_INSERT_ID();
INSERT INTO four (three_id, title) values (#last_id_three, 'Some text')
SET #last_id_four = LAST_INSERT_ID();
INSERT INTO five (four_id, title) values (#last_id_four, 'Some text')
The only one explanation I thought about was that it's somehow related to the variable names in the recursive function and therefore I renamed all of vars but it didn't resolve the issue.
Below I show the full code of this recursive function
/*
Recursive function for inserting or editing nested data (since 4th until nth level)
$subTable - current table where we insert new or edit existing data
$subData - current table's data in form view
$existingSubJoin - current table data that is already in database (submitted earlier)
$existing... - corresponding variables for existing data
$parentTable - current table's parent table (where current tables FK is pointing)
$existingParentJoin - parent table data that already exists
$parentPkField, $parentPkValue - the names are self-explanatory
$parentPkValue can be a real number from existing row or #last_id_$parentTable
#last_id_$subTable - a MySQL variable that passes the last_insert_id() value from newly submitted parent row to the child row's FK
$subSingle - a new array of db field values for one row
$subSet - array for UPDATE statements (SET field = 'value', field2 = 'value2' etc)
$subFields - array of fields for INSERT INTO
$nextLastId = pk value or #last_id_$subTable to be passed as a last argument for next recursion
*/
public function buildQueryListChild($subTable, $subData, $existingSubJoin, $parentTable, $existingParentJoin, $parentPkField, $parentPkValue)
{
if (isset($subData))
{
foreach($subData as $sKey => $subRow)
{
$subSingle = array();
if (!isset($existingSubJoin['rows'][$sKey]))
{
$existingSubRow = $existingSubJoin['rows'][0];
}
else
{
$existingSubRow = $existingSubJoin['rows'][$sKey];
}
$subSet = array();
$subParentId = $parentTable . '_' . $parentPkField;
foreach ($subRow as $subField => $subValue)
{
if (isset($existingSubJoin['properties']['fields']))
{
foreach ($existingSubJoin['properties']['fields'] as $existingSubField)
{
if ($existingSubField['name'] == $subField)
{
if ($existingSubField['key'] == 'PRI')
{
$subRowPkField = $existingSubField['name'];
$subRowPkAlias = $existingSubField['alias'];
}
else
{
$subRowField = $existingSubField['name'];
$subRowAlias = $existingSubField['alias'];
$subRowType = $existingSubField['type'];
}
$sNumTypes = array('int', 'float', 'decimal', 'numeric', 'double', 'bit');
foreach ($sNumTypes as $sType)
{
$sNumber = strpos($existingSubField['type'], $sType) === true ? true : null;
}
$sString = $sNumber ? false : true;
}
}
}
if (empty($subRow[$subRowPkField]))
{
$newSub = true;
$updateSub = false;
}
else
{
$updateSub = true;
$newSub = false;
}
if (!is_array($subValue))
{
if ($subField != $subRowPkField && strpos($subRowType, 'timestamp') === false)
{
if ($subField == $subParentId)
{
$subSingle[$subParentId] = $parentPkValue;
}
else
{
if (!empty($subValue)) $subSingle[$subField] = $subValue;
}
if ($updateSub && $subField == $subRowField && $subSingle[$subField] != $existingSubRow['data'][$subRowAlias])
{
$uSubField = $subField;
$uSubValue = $subValue;
if (!$sNumber)
{
$uSubValue = "'$subValue'";
}
$subSet[$uSubField] = "$uSubField = $uSubValue";
}
}
}
}
if (!empty($subSet))
{
$subSets = implode(', ', $subSet);
$subRowPkValue = $subRow[$subRowPkField];
$current = "UPDATE $subTable SET $subSets WHERE $subRowPkField = $subRowPkValue;\n";
$sql .= $current;
}
if ($newSub)
{
$subRowPkValue = $subRow[$subRowPkField];
if (!empty($subSingle))
{
$subFields = implode(', ', array_keys($subSingle));
$subValues = "'" . implode("', '", array_values($subSingle)) . "'";
$subValues = str_replace("'$parentPkValue'", "$parentPkValue", $subValues);
$current = "INSERT INTO $subTable ($subFields) VALUES ($subValues);\n";
$sql .= $current;
$sql .= "SET #last_id_$subTable = LAST_INSERT_ID();\n";
}
}
foreach ($subRow as $sTable => $sData)
{
if (is_array($sData))
{
if (isset($existingSubJoin['rows'][$sKey]) && $sKey > 0)
{
$nextLastId = $sKey;
}
else
{
$nextLastId = "#last_id_$subTable";
}
$existingSData = $existingSubRow['joins']->$sTable;
$sql .= $this->buildQueryListChild($sTable, $sData, $existingSData, $subTable, $existingSubJoin, $subRowPkField, $nextLastId);
}
}
}
}
return $sql;
}
You see there a line
$current = "INSERT INTO $subTable ($subFields) VALUES ($subValues);\n";
where both $subFields and $subValues are imploded from corresponding submission array (array_keys and array_values) that is created in
if ($subField == $subParentId)
{
$subSingle[$subParentId] = $parentPkValue;
}
else
{
if (!empty($subValue)) $subSingle[$subField] = $subValue;
}
And as said, ($subFields) should always contain parenttable_id and ($subValues) its existing value or #last_id_parenttable
Sorry for this amount of information and thanks in advance for help!
SOLUTION FOUND - see ANSWER
There were also other issues that occurred in my code but, like changes I made in parent function, this is outside of this issue's scope. I hope all my explanations are clear :)
The (main?) cause was that the parent pk field name was not always passed to child. I discovered this when I got idea that the way how the fk value was set was not the best one and that this should be done at very beginning, before creating a new array. So the first thing I did was that I moved it right before iterating the fields
$subParentId = $parentTable . '_' . $parentPkField;
$subRow[$subParentId] = $parentPkValue;
foreach ($subRow as $subField => $subValue)
{ ... }
But it wasn't enough. Yes, I got the needed value but without $parentPkField so I got a nonexisting field name (parenttablename_). Therefore it was clear what I should really look for.
And I got it. Some time ago I built among others a "blank row" feature to be used in some cases when there is no real db row. In this case I just forgot to use it where needed :D
Therefore I had to make some corrections also to the parent function and pass an additional $blank array from there to the child. And of course corresponding changes to the current function. This way the existence of $parentPkField was ensured.
The diffs in this function are here (old commented, new below or otherwise explained)
/*
public function buildQueryListChild($subTable, $subData, $existingSubJoin, $parentTable, $existingParentJoin, $parentPkField, $parentPkValue)
*/
public function buildQueryListChild($subTable, $subData, $existingSubJoin, $blank, $parentTable, $existingParentJoin, $parentPkField, $parentPkValue)
{
.....
if (!isset($existingSubJoin['rows'][$sKey]))
{
// $existingSubRow = $existingSubJoin['rows'][0];
$existingSubRow = $blank['rows'][0];
}
......
// Added
$subRow[$subParentId] = $parentPkValue;
......
foreach ($subRow as $subField => $subValue)
{
// Added
if (!$existingSubJoin) $existingSubJoin = $blank;
......
if ($subField != $subRowPkField && strpos($subRowType, 'timestamp') === false)
{
/*
if ($subField == $subParentId)
{
$subSingle[$subParentId] = $parentPkValue;
}
else
{
if (!empty($subValue)) $subSingle[$subField] = $subValue;
}
*/
if (!empty($subValue)) $subSingle[$subField] = $subValue;
.......
if ($newSub)
{
// Removed as unneeded
//$subRowPkValue = $subRow[$subRowPkField];
if (!empty($subSingle))
{
........
// if / else moved here, see below foreach
if (isset($existingSubJoin['rows'][$sKey]) && $sKey > 0)
{
$nextLastId = $sKey;
}
else
{
$nextLastId = "#last_id_$subTable";
}
foreach ($subRow as $sTable => $sData)
{
if (is_array($sData))
{
/*
if (isset($existingSubJoin['rows'][$sKey]) && $sKey > 0)
{
$nextLastId = $sKey;
}
else
{
$nextLastId = "#last_id_$subTable";
}
*/
// The commented lines above: moved them before foreach but actually not sure if it had any impact
//Added
$nextBlank = $blank['rows'][0]['joins']->$sTable;
/*
$sql .= $this->buildQueryListChild($sTable, $sData, $existingSData, $subTable, $existingSubJoin, $subRowPkField, $nextLastId);
*/
$sql .= $this->buildQueryListChild($sTable, $sData, $existingNextData, $nextBlank, $subTable, $existingSubJoin, $subRowPkField, $nextLastId);

How can I add records on an empty table

I have an empty table in my database in MySQL, and I trying to add multiple records
(I do this in this way because I'm following and order; If I delete a record then the auto increment field (id) skips 1 value. For example. I delete the id = 340 and then the following record start with an id with a value = 341)
I have an If Else statement in a function in my controller where I use to compare the id itself of my table Hours.
public function showHours($id){
$complex = ComplexNew::find($id);
$fields = CourtsComplex::all();
$hours = HoursNew::all();
$last_hours = collect($hours)->last();
if ($last_hours->id == $last_hours->id){
$last_hours->id = $last_hours->id + 1;
}else{
return Redirect()->back();
}
return view('hours.FormNewHours')->with('complex', $complex)->with('fields', $fields)->with('last_hours', $last_hours);
}
And this line is the line where I have the error.
if ($last_hours->id == $last_hours->id){
//DO SOMETHING
// ...
}
The error is: 'Trying to get property 'id' of non-object'.
Also I was trying to add another if else statement something like this:
if(is_null($last_hours->id)){
$last_hours->id = 1;
}else if($last_hours->id == $last_hours->id){
//ADD +1 TO ID.
}
Because I want that if the table is empty the id of the first record must be started with value = 1 but if the table is not empty add 1 to the last id, like a for statement because this condition add always +1 to last id.
Change
$last_hours = collect($hours)->last();
to
$last_hours = $hours->last();
Plus dd($last_hours) if it returns object then try accessing id like $last_hours->id

Return result in a looped function

I have a function that I loop to to build an url. what I need is to get the result in a single string. This is the code.
// Create URL for accesories
function create_url($id) {
global $db_categories;
// Select category sublevels
$sql_cat="SELECT parent_id, seo_id FROM $db_categories WHERE category_id = '".$id."'";
$catres = mysql_query("$sql_cat") or die (mysql_error());
while($selected_cat = mysql_fetch_array($catres)) {
$cat_seo = $selected_cat['seo_id']. "/";
$output .= $cat_seo;
create_url($selected_cat['parent_id']);
}
return $output;
}
// Call function
$result_cat = create_url($cat["category_id"]);
echo $result_cat;
This work fine if I use echo and will output (I only use $depth to track the results)
category1/
category2/
category3/
etc...
The problem is that I don't know how to return the result into a single string instead of echo it. When I use return it only outputs the first result. like this.
category1/
I want the return to output.
category1/category2/category3/
I can't for my life find a solution for this.
Thank's

PHP prevent double clean url (improvements?)

For a client at work we have build a website.The website has an offering page which can contain variants of the same type/build, so they ran into problems with double clean-urls.
Just now I wrote a function to prevent that from happening by appending a number to the URL. If thatclean url also exists it counts up.
E.g.
domain.nl/product/machine
domain.nl/product/machine-1
domain.nl/product/machine-2
Updated! return $clean_url; on recursion and on return
The function I wrote works fine, but I was wondering if I have taken the right approach and if it maybe could be improved. Here's the code:
public function prevent_double_cleanurl($cleanurl)
{
// makes sure it doesnt check against itself
if($this->ID!=NULL) $and = " AND product_ID <> ".$this->ID;
$sql = "SELECT product_ID, titel_url FROM " . $this->_table . " WHERE titel_url='".$cleanurl."' " . $and. " LIMIT 1";
$result = $this->query($sql);
// if a matching url is found
if(!empty($result))
{
$url_parts = explode("-", $result[0]['titel_url']);
$last_part = end($url_parts);
// maximum of 2 digits
if((int)$last_part && strlen($last_part)<3)
{
// if a 1 or 2 digit number is found - add to it
array_pop($url_parts);
$cleanurl = implode("-", $url_parts);
(int)$last_part++;
}
else
{
// add a suffix starting at 1
$last_part='1';
}
// recursive check
$cleanurl = $this->prevent_double_cleanurl($cleanurl.'-'.$last_part);
}
return $cleanurl;
}
Depending on the likeliness of a "clean-url" being used multiple times, your approach may not be the best to roll with. Say there was "foo" to "foo-10" you'd be calling the database 10 times.
you also don't seem to sanitize the data you shove into your SQL queries. Are you using mysql_real_escape_string (or its mysqli, PDO, whatever brother)?
Revised code:
public function prevent_double_cleanurl($cleanurl) {
$cleanurl_pattern = '#^(?<base>.*?)(-(?<num>\d+))?$#S';
if (preg_match($cleanurl_pattern, $base, $matches)) {
$base = $matches['base'];
$num = $matches['num'] ? $matches['num'] : 0;
} else {
$base = $cleanurl;
$num = 0;
}
// makes sure it doesnt check against itself
if ($this->ID != null) {
$and = " AND product_ID <> " . $this->ID;
}
$sql = "SELECT product_ID, titel_url FROM " . $this->_table . " WHERE titel_url LIKE '" . $base . "-%' LIMIT 1";
$result = $this->query($sql);
foreach ($result as $row) {
if ($this->ID && $row['product_ID'] == $this->ID) {
// the given cleanurl already has an ID,
// so we better not touch it
return $cleanurl;
}
if (preg_match($cleanurl_pattern, $row['titel_url'], $matches)) {
$_base = $matches['base'];
$_num = $matches['num'] ? $matches['num'] : 0;
} else {
$_base = $row['titel_url'];
$_num = 0;
}
if ($base != $_base) {
// make sure we're not accidentally comparing "foo-123" and "foo-bar-123"
continue;
}
if ($_num > $num) {
$num = $_num;
}
}
// next free number
$num++;
return $base . '-' . $num;
}
I don't know about the possible values for your clean-urls. Last time I did something like this, my base could look like some-article-revision-5. That 5 being part of the actual bullet, not the duplication-index. To distinguish them (and allow the LIKE to filter out false positives) I made the clean-urls look like $base--$num. the double dash could only occur between the base and the duplication-index, making things a bit simpler…
I have no way to test this, so its on you, but here's how I'd do it. I put a ton of comments in there explaining my reasoning and the flow of the code.
Basically, the recursion is unnecessary will result in more database queries than you need.
<?
public function prevent_double_cleanurl($cleanurl)
{
$sql = sprintf("SELECT product_ID, titel_url FROM %s WHERE titel_url LIKE '%s%%'",
$this->_table, $cleanurl);
if($this->ID != NULL){ $sql.= sprintf(" AND product_ID <> %d", $this->ID); }
$results = $this->query($sql);
$suffix = 0;
$baseurl = true;
foreach($results as $row)
{
// Consider the case when we get to the "first" row added to the db:
// For example: $row['titel_url'] == $cleanurl == 'domain.nl/product/machine'
if($row['title_url'] == $cleanurl)
{
$baseurl = false; // The $cleanurl is already in the db, "this" is not a base URL
continue; // Continue with the next iteration of the foreach loop
}
// This could be done using regex, but if this works its fine.
// Make sure to test for the case when you have both of the following pages in your db:
//
// some-hyphenated-page
// some-hyphenated-page-name
//
// You don't want the counters to get mixed up
$url_parts = explode("-", $row['titel_url']);
$last_part = array_pop($url_parts);
$cleanrow = implode("-", $url_parts);
// To get into this block, three things need to be true
// 1. $last_part must be a numeric string (PHP Duck Typing bleh)
// 2. When represented as a string, $last_part must not be longer than 2 digits
// 3. The string passed to this function must match the string resulting from the (n-1)
// leading parts of the result of exploding the table row
if((is_numeric($last_part)) && (strlen($last_part)<=2) && ($cleanrow == $cleanurl))
{
$baseurl = false; // If there are records in the database, the
// passed $cleanurl isn't the first, so it
// will need a suffix
$suffix = max($suffix, (int)$last_part); // After this foreach loop is done, $suffix
// will contain the highest suffix in the
// database we'll need to add 1 to this to
// get the result url
}
}
// If $baseurl is still true, then we never got into the 3-condition block above, so we never
// a matching record in the database -> return the cleanurl that was passed here, no need
// to add a suffix
if($baseurl)
{
return $cleanurl;
}
// At least one database record exists, so we need to add a suffix. The suffix we add will be
// the higgest we found in the database plus 1.
else
{
return sprintf("%s-%d", $cleanurl, ($suffix + 1));
}
}
My solution takes advantage of SQL wildcards (%) to reduce the number of queries from n down to 1.
Make sure that you ensure problematic case I described in lines 14-20 works as expected. Hyphens in the machine name (or whatever it is) could do unexpected things.
I also used sprintf to format the query. Make sure you sanitize any string that is passed through as a string (e.g. $cleanurl).
As #rodneyrehm points out, PHP is very flexible with what it considers a numeric string. You might consider switching out is_numeric() for ctype_digit() and see how that works.

Categories