PHP Testing JSON Contains with SQLite - php

My Web App uses Laravel & MySQL but for my tests I am using SQLite in memory.
This is my code that I use in my Controller:
$opportunities = DB::table('opportunities')
->whereRaw('JSON_CONTAINS(businesses, \'"' . $business . '"\')')
->get();
When testing this throws an exception because of how SQLite doesn't have a JSON_CONTAINS function. How can I get around this so that my tests pass and I don't have to make any massive changes to the structure? Does SQLite have a function for this or something along these lines?
Thanks

You could emulate JSON_CONTAINS during testing using sqlite_create_function e.g.
function json_contains($json, $val) {
$array = json_decode($json, true);
// trim double quotes from around the value to match MySQL behaviour
$val = trim($val, '"');
// this will work for a single dimension JSON value, if more dimensions
// something more sophisticated will be required
// that is left as an exercise for the reader
return in_array($val, $array);
}
sqlite_create_function(<your db handle>, 'JSON_CONTAINS', 'json_contains');
You may also want to emulate the optional third parameter of JSON_CONTAINS e.g.
function json_contains($json, $val, $path = null) {
$array = json_decode($json, true);
// trim double quotes from around the value to match MySQL behaviour
$val = trim($val, '"');
// this will work for a single dimension JSON value, if more dimensions
// something more sophisticated will be required
// that is left as an exercise for the reader
if ($path)
return $array[$path] == $val;
else
return in_array($val, $array);
}

A bit more explanation for the accepted answer:
/**
* Call this method in your test.
* #PHP 7.4+
*/
private function setupSqlite(): void
{
DB::connection()->getPdo()->sqliteCreateFunction('JSON_CONTAINS', function ($json, $val, $path = null) {
$array = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
// trim double quotes from around the value to match MySQL behaviour
$val = trim($val, '"');
// this will work for a single dimension JSON value, if more dimensions
// something more sophisticated will be required
// that is left as an exercise for the reader
if ($path) {
return $array[$path] == $val;
}
return in_array($val, $array, true);
});
Then you can use this query, that works for both mysql/mariadb and sqlite:
$result = Product::whereRaw('JSON_CONTAINS(tags, \'"' . $tag . '"\')')->get();
In above example I was searching json column with simple array:
$column = ["example", "test", "simple"];

I gave up on running my test suite on SQLite because there are just too many differences between the implementations. The first issue was it's lack of support for enums, but when even simple things like ALTER statements were causing problems I changed to MySQL for testing too.
What I do now is specify the name of my testing db in phpunit.xml.
<phpunit>
<php>
<env name="DB_DATABASE" value="mydb_testing"/>
</php>
</phpunit>
The down side of this is being required to create two databases to develop the app, but I need to be sure that the migrations will work on production, which uses MySQL and testing on SQLite doesn't prove that.

Related

How do i separate my array strings delimiter (|) using implode function of php

How do I separate my array strings delimiter (|) using the implode function of PHP something like the below String
|Java||PHP||Bootstrap||HTML||CSS|
Actually, I am using a double delimiter to differentiate tags like SQL and MySQL because LIKE "%sql%" will return MySQL results as well. Should be LIKE "%|sql|%"
What I have tried:
$array_service_offer = array();
if (isset($_POST['service_offer'])) {
foreach ($_POST['service_offer'] as $selectedOption) {
array_push($array_service_offer, $selectedOption);
}
//$service_offer = implode(',',$array_service_offer);
$service_offer = '|' . implode('||', $array_service_offer) . '|';
} else {
$service_offer = "";
}
First of all, according to #Qirel comment, I would also recommend to use $array_service_offer[] = $selectedOption; instead of array_push($array_service_offer, $selectedOption);
now for separation, there are several solutions.
One solution is that:
1- to remove first and last | character (it is like trimming)
2- to explode the trimmed string using || delimiter
for that you may use the following code:
$service_offer_trimmed = preg_replace("~(^\|)|(\|$)~", "", $service_offer);
$service_offer_array = explode('||', $service_offer_trimmed);
The other solution is to use straight forward preg_replace function to separate the string. the command follows:
$service_offer_array = preg_split("~(^\|)|(\|\|)|(\|$)~", $service_offer, 0, PREG_SPLIT_NO_EMPTY);
And one more professional solution is that to store your data in database in JSON format rather than delimited code and then when you need to search in your database you may use MySql JSON_CONTAINS function rather than LIKE command.
I have not personally made a performance check on both two solutions but if it not a big database, then it is not a big concern as well.
Therefore, you initial code to get the data and store it into the database will be:
$array_service_offer = array();
if (isset($_POST['service_offer'])) {
foreach ($_POST['service_offer'] as $selectedOption) {
$array_service_offer[] = $selectedOption;
}
}
// $json_service_offer will be saved to the database
$json_service_offer = json_encode($array_service_offer);
the manual on how to use JSON_CONTAINS is in the following link:
12.17.3 Functions That Search JSON Values

PHP/MySQL include alternative spellings in search

My PHP search form pulls data from a MySQL database. I am expecting users to sometimes fill the search box with a search term that has a slightly different spelling than my database entry, like "theater" instead of "theater." There are just a few of these that I expect to be very common, so I added an additional row to my database table that contains those alternative spellings, and my PHP search form searches this row of the database as well. It works well, but this will cause a lot of additional work when maintaining the database, so I'm wondering if there's something I can do within my PHP code to search for those predefined alternative spellings (I don't mean to give the user suggested spellings, but I want the search form to return, for example, entries that have "theatre" in it even though the user typed "theater." Is there an easy way to do this (without a search server)?
Yes you can easily do this work without database search,you need correct spellings so I suggest you do this work from PHP coding instead of databases search...
You can do this work with PHP Pspell module, PHP Pspell work like android keyboard whenever use type wrong spelling in search box it automatically check that spelling from dictionary and make it correct like if user type "theater" then it automatically correct it with "theatre".
before starting programming you have to check is Pspell module installed or not
<?php
$config_dic= pspell_config_create ('en');
Here is a small function to help you understand how Pspell works:
<?php
function orthograph($string)
{
// Suggests possible words in case of misspelling
$config_dic = pspell_config_create('en');
// Ignore words under 3 characters
pspell_config_ignore($config_dic, 3);
// Configure the dictionary
pspell_config_mode($config_dic, PSPELL_FAST);
$dictionary = pspell_new_config($config_dic);
// To find out if a replacement has been suggested
$replacement_suggest = false;
$string = explode('', trim(str_replace(',', ' ', $string)));
foreach ($string as $key => $value) {
if(!pspell_check($dictionary, $value)) {
$suggestion = pspell_suggest($dictionary, $value);
// Suggestions are case sensitive. Grab the first one.
if(strtolower($suggestion [0]) != strtolower($value)) {
$string [$key] = $suggestion [0];
$replacement_suggest = true;
}
}
}
if ($replacement_suggest) {
// We have a suggestion, so we return to the data.
return implode('', $string);
} else {
return null;
}
}
To use this function, it is sufficient to pass to it a string parameter:
<?php
$search = $_POST['input'];
$suggestion_spell = orthograph($search);
if ($suggestion_spell) {
echo "Try with this spelling : $suggestion_spell";
}
$dict = pspell_new ("en");
if (!pspell_check ($dict, "lappin")) {
$suggestions = pspell_suggest ($dict, "lappin");
foreach ($suggestions as $suggestion) {
echo "Did you mean: $suggestion?<br />";
}
}
// Suggests possible words in case of misspelling
$config_dic = pspell_config_create('en');
// Ignore words under 3 characters
pspell_config_ignore($config_dic, 3);
// Configure the dictionary
pspell_config_mode($config_dic, PSPELL_FAST);
$dictionary = pspell_new_config($config_dic);
$config_dic = pspell_config_create ('en');
pspell_config_personal($config_dic, 'path / perso.pws');
pspell_config_ignore($config_dic , 2);
pspell_config_mode($config_dic, PSPELL_FAST);
$dic = pspell_new_config($config_dic);
pspell_add_to_personal($dic, "word");
pspell_save_wordlist($dic);
?>

Using Predis to store form data

I tried using lpush
$list = "flavors";
$array = array($_GET["mainid"], $_GET["flavor1"], $_GET["flavor2"], $_GET["flavor3"]);
$redis = new Predis\Client();
$redis->lpush($list,implode("", $array));
echo $redis->lrange(0, -1);
I have tried using hset
$redis->hset("flavors", $_GET["mainid"], $_GET["mainid"]);
$redis->hset("flavors", $_GET['mainid'] . "flavor1", $_GET["flavor1"]);
$redis->hset("flavors", $_GET['mainid'] . "flavor2", $_GET["flavor2"]);
$redis->hset("flavors", $_GET['mainid'] . "flavor3", $_GET["flavor3"]);
echo $redis->hgetall($_GET['mainid']);
But I can't get that to work because I get this error: http://pastie.org/8401717
How could I fix that? I think it is something about being given an array when it expects a string, but I have implode in there, so why else isn't it working? If it can't work at all, what other Redis data type could I use?
You got that error when using lpush because the flavors key is already stored with a different Redis data type. So you should delete that key before you try again.
You also used lpush in wrong way. You should try this:
foreach ($array as $value) {
$redis->lpush($list, $value);
}
Or if your redis api support multi params:
call_user_func_array(array($redis, 'lpush'), array_merge($list, $array));
If you want to store flavors by mainid, you may want to store it with multi keys and use lpush:
$list = "flavor:{$_GET['mainid']}";
$redis->lpush($list, $_GET["flavor1"]);
$redis->lpush($list, $_GET["flavor2"]);
$redis->lpush($list, $_GET["flavor3"]);
Another way is to store in a single hash and using json_encode (don't use implode):
$data = json_encode(array($_GET["flavor1"], $_GET["flavor2"], $_GET["flavor3"]));
$redis->hset('flavors', $_GET["mainid"], $data);

Format CSV into json to be consumed by ui.autocomplete

I have php problem with formatting output from a CSV to make it available as json for jquery ui.autocomplete.
fruit.csv:
apple, bananna, jackfruit,
... etc
jquery from here:
http://jqueryui.com/demos/autocomplete/#default
$( "#fruits" ).autocomplete({
source: '/path/to/fruit.json'
});
PHP to convert CSV into json:
// Callback function to output CSV as json object
function _custom_json_from_csv() {
$fruit_path = '/path/to/fruit.csv';
$fruits = array_map("str_getcsv", file($fruit_path));
drupal_json_output(array(array_values($fruits)));
exit;
}
// Below are CMS codes for detailed illustration
function drupal_json_output($var = NULL) {
// We are returning JSON, so tell the browser.
drupal_add_http_header('Content-Type', 'application/json');
if (isset($var)) {
echo drupal_json_encode($var);
}
}
function drupal_json_encode($var) {
// The PHP version cannot change within a request.
static $php530;
if (!isset($php530)) {
$php530 = version_compare(PHP_VERSION, '5.3.0', '>=');
}
if ($php530) {
// Encode <, >, ', &, and " using the json_encode() options parameter.
return json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
}
// json_encode() escapes <, >, ', &, and " using its options parameter, but
// does not support this parameter prior to PHP 5.3.0. Use a helper instead.
include_once DRUPAL_ROOT . '/includes/json-encode.inc';
return drupal_json_encode_helper($var);
}
// parts of json-encode.inc - drupal_json_encode_helper(), responsible for json output:
case 'array':
// Arrays in JSON can't be associative. If the array is empty or if it
// has sequential whole number keys starting with 0, it's not associative
// so we can go ahead and convert it as an array.
if (empty($var) || array_keys($var) === range(0, sizeof($var) - 1)) {
$output = array();
foreach ($var as $v) {
$output[] = drupal_json_encode_helper($v);
}
return '[ ' . implode(', ', $output) . ' ]';
}
// Otherwise, fall through to convert the array as an object.
case 'object':
$output = array();
foreach ($var as $k => $v) {
$output[] = drupal_json_encode_helper(strval($k)) . ':' . drupal_json_encode_helper($v);
}
return '{' . implode(', ', $output) . '}';
If there is a solution to directly consume CSV by jquery, that will be great. But no clue by now.
My problem is function _custom_json_from_csv() outputs non-expected format for ui.autocomplete. Note excessive of [[[...]]]:
[[["apple", "bananna", "jackfruit"]]]
While ui.autocomplete wants:
["apple", "bananna", "jackfruit"]
Any direction to format the function as expected by jquery ui.autocomplete?
PS: I don't use #autocomplete_path form API, and instead using ui.autocomplete, for reasons:
1) the code is stored in a theme settings, no hook_menu is available by theme, I want to avoid a module for this need whenever possible.
2) There is a plan somewhere at d.o. to use ui.autocomplete, so consider this adventurous
3) My previous question from jquery viewpoint has led me to instead correct the output of json, rather than making jquery adapt to json.
4) This is more my php issue rather than drupal
Thanks
UPDATE:
Removing one array from drupal_json_output(array(array_values($fruits))); to drupal_json_output(array_values($fruits)); successfully reduced one [] (what is the name of this?). Obviously a miss from previous format with the leading group.
[["apple", "bananna", "jackfruit"]]
I need to remove one more []
I think your code is "correct", if you had:
apple, bananna, jackfruit
peanut, walnut, almont
carrot, potato, pea
then your function would end up as ... etc
[[apple, bananna, jackfruit],[peanut, walnut, almont],[carrot, potato, pea]]
Which appears sensible.
If you only want one row, why can't you just use the result of
$FileContents = file($fruit_path);
$fruits = str_getcsv($FileContents[0]);
as that will turn an array of values in the first row, not an array of arrays of all the rows
Maybe simple is using in js array[0][0]( for drupal_json_output(array(array_values($fruits)));
in php) or array[0] in js( for drupal_json_output(array_values($fruits));) in php

PHP json_encode a debug_backtrace() with resource types

Currently, I have a logger which logs errors together with a backtrace.
The logger serializes the backtrace to JSON via json_encode().
Let's look at some hypothetical code...
<?php
error_reporting(-1); // show all errors
function test($b){
echo json_encode(debug_backtrace()); // take a backtrace snapshot
}
$c = imagecreate(50,50); // create a resource...
test($c); // ...and pass to function
?>
If you run the code above, we will see something like:
Warning: json_encode() [function.json-encode]: type is unsupported, encoded as null in /code/ch6gVw on line 5
[{"file":"/code/ch6gVw","line":8,"function":"test","args":[null]}]
We can notice two things going on here:
The logger itself is causing a warning! Bad bad bad!
The logged data tells us we passed a null to the function?!?!
So, my proposed solution is something like:
foreach($trace as $i=>$v)
if(is_resource($v))
$trace[$i] = (string)$v.' ('.get_resource_type($v).')';
The result would look like Resource id #1 (gd)
This, however, may cause some grave issues.
We need to somehow track which arrays we looped through so as to avoid ending up in infinite loops with arrays referencing themselves ($GLOBALS tend to cause this mess).
We would also have to convert resources of object properties, but objects, unlike arrays, are not a copy of the original thing, hence changing the property changes the live object. On the other hand, how safe is it to clone() the object?
Won't such a loop severely slow down the server (backtraces tend to be large, no)?
I ended up with the following function:
function clean_trace($branch){
if(is_object($branch)){
// object
$props = array();
$branch = clone($branch); // doesn't clone cause some issues?
foreach($props as $k=>$v)
$branch->$k = clean_trace($v);
}elseif(is_array($branch)){
// array
foreach($branch as $k=>$v)
$branch[$k] = clean_trace($v);
}elseif(is_resource($branch)){
// resource
$branch = (string)$branch.' ('.get_resource_type($branch).')';
}elseif(is_string($branch)){
// string (ensure it is UTF-8, see: https://bugs.php.net/bug.php?id=47130)
$branch = utf8_encode($branch);
}
// other (hopefully serializable) stuff
return $branch;
}
You can see it in action here. However, I'm not convinced:
It is quite slow (iterating over lots of data)
It is quite memory intensive (data needs to be copied to not mess the original)
It is not safe in case where arrays/objects reference themselves
Example: $a = array(); $a['ref'] = &$a; (PHP does this to some internal variables)
I'm concerned that cloning objects may have some serious side-effects (consider the magic method __clone(), an invitation to wreck havoc).
So you are trying to store the backtrace as a data structure that can be used to pretty-print the results later on?
If that isn't needed I'd just store $result = print_r(debug_backtrace(), true) and be done with it.
If not my first shot would be something like:
<?php
error_reporting(-1);
function test($b){
echo json_encode(clean(debug_backtrace()));
}
$c = fopen("/tmp/foo", "w");
test($c);
function clean($trace) {
array_walk_recursive($trace, function(&$element) {
if(is_object(&$element)) {
// work around unrealizable elements and preserve typing
$element = array(get_class($element), (object)$element);
} else if(is_resource($element)) {
$element = get_resource_type($element) . '#' .(int)$element;
}
});
return $trace;
}
It's just a rough sketch but I'm not aware of any project that stores backtracks for later inspection in a non textual or already processed format and looking around the mature frameworks didn't bring anything up

Categories