For my API we are accepting loads of JSON data. Sometimes there is missing data when we refer to it. So given the following JSON that is posted to the API:
{
"reference_id": "6599",
"balance_0_30": "0",
"balance_31_60": "0",
"balance_over_90": "0",
"account_balance": "0"
}
As I loop over it like this:
foreach ($request->input('data') as $record) {
$record = (object) $record;
$accounting->reference_id = isset($record->reference_id) ? $record->reference_id : NULL;
$accounting->reference_guarantor_id = $record->reference_guarantor_id ?: NULL;
$accounting->balance_0_30 = isset($record->balance_0_30) ? $record->balance_0_30 : NULL;
$accounting->balance_31_60 = isset($record->balance_31_60) ? $record->balance_31_60 : NULL;
$accounting->balance_61_90 = isset($record->balance_61_90 ) ? $record->balance_61_90 : NULL;
$accounting->balance_over_90 = isset($record->balance_over_90) ? $record->balance_over_90 : NULL;
$accounting->account_balance = isset($record->account_balance) ? $record->account_balance : NULL;
This works, but it is rather "messy" to read, and I have about 4000 lines of similar code and growing.
The issue is that if I send up JSON data without the account_balance declared, I get the error:
Undefined property: stdClass
I was thinking I could write a tiny function like this:
function i($value) {
if($value!=null){
if(is_int($value)){
return $value;
}
if(is_float($value)){
return $value;
}
}
return 0;
}
Where if I knew that column would be an integer or float, I could call it like this:
$accounting->account_balance = i($record->account_balance);
Then if the value was null, it would just fill in a 0 and not error out. That would make things much easier to read, troubleshoot and so on. Trouble is that the Exception is thrown before it gets to the i function.
I tried using the set_exception_handler() as described here, including the class example from Glen: https://www.php.net/manual/en/function.set-exception-handler.php but it didn't work.
Am I out of luck, or is there a way to do what I want?
You could just do this:
foreach ($request->input('data') as $record) {
Accounting::create($record);
}
Your database columns should be nullable and do not forget to set $fillable attribute in your Accounting model (For this you can set protected $guarded = ['id', 'created_at', 'updated_at']; in your model to consider all other columns as fillable).
Related
I just found something "kinda strange" about PHP 7.4 and I am not sure if it is just me missing something or maybe if it is an actual bug. Mostly I am interested in your opinion/confirmation.
So in PHP, you can iterate over objects properties like this:
class DragonBallClass
{
public $goku;
public $bulma = 'Bulma';
public $vegeta = 'Vegeta';
}
$dragonBall = new DragonBallClass();
foreach ($dragonBall as $character) {
var_dump($character);
}
RESULT
NULL
string(5) "Bulma"
string(6) "Vegeta"
Now if we start using strongly typed properties like that:
class DragonBallClass
{
public string $goku;
public string $bulma = 'Bulma';
public string $vegeta = 'Vegeta';
}
$dragonBall = new DragonBallClass();
foreach ($dragonBall as $character) {
var_dump($character);
}
We will get a different result:
string(5) "Bulma"
string(6) "Vegeta"
Now what is different:
When you DO NOT assign a default value to strongly typed property it will be of Uninitialized type. Which of course makes sense. The problem is though that if they end up like this you cannot loop over them they will simply be omitted - no error, no anything as you can see in the second example. So I just lose access to them.
It makes sense but just imagine that you have a custom Request/Data class like this:
namespace App\Request\Request\Post;
use App\Request\Request\Request;
class GetPostsRequest extends Request
{
public string $title = '';
}
Do you see that ugly string assignment? If I want to make my properties on the class iterable then I have to either:
drop types
assign dummy values
I might want to have an object with typed properties without any values in them to loop over them and populate them if that makes sense.
Is there any better way of doing this? Is there any option to keep types and keep em iterable without having to do this dummy value abomination?
If you want to allow a typed attribute to be nullable you can simply add a ? before the type and give NULL as default value like that:
class DragonBallClass
{
public ?string $goku = NULL;
public string $bulma = 'Bulma';
public string $vegeta = 'Vegeta';
}
In this case NULL is a perfectly legitimate value (and not a dummy value).
demo
Also without using ?, you can always merge the class properties with the object properties lists:
class DragonBallClass
{
public string $goku;
public string $bulma = 'Bulma';
public string $vegeta = 'Vegeta';
}
$dragonBall = new DragonBallClass();
$classProperties = get_class_vars(get_class($dragonBall));
$objectProperties = get_object_vars($dragonBall);
var_dump(array_merge($classProperties, $objectProperties));
// array(3) {
// ["goku"]=>
// NULL
// ["bulma"]=>
// string(5) "Bulma"
// ["vegeta"]=>
// string(6) "Vegeta"
// }
Before we start - I think that the answer accepted by me and provided by Casimir is better and more correct than what I came up with(that also goes for the comments).
I just wanted to share my thoughts and since this is a working solution to some degree at least we can call it an answer.
This is what I came up with for my specific needs and just for fun. I was curious about what I can do to make it more the way I want it to be so don't freak out about it ;P I think that this is a quite clean workaround - I know it's not perfect though.
class MyRequest
{
public function __construct()
{
$reflectedProperties = (new \ReflectionClass($this))->getProperties();
foreach ($reflectedProperties as $property) {
!$property->isInitialized($this) ??
$property->getType()->allowsNull() ? $property->setValue($this, null) : null;
}
}
}
class PostRequest extends MyRequest
{
public ?string $title;
}
$postRequest = new PostRequest();
// works fine - I have access here!
foreach($postRequest as $property) {
var_dump($property);
}
The downfall of this solution is that you always have to make types nullable in your class. However for me and my specific needs that is totally ok. I don't mind, they would end up as nullables anyway and it might be a nice workaround for a short deadline situation if someone is in a hurry.
It still keeps the original PHP not initialized error though when the type is not nullable. I think that is actually kinda cool now. You get to keep all the things: Slim and lean classes, PHP error indicating the true nature of the problem and possibility to loop over typed properties if you agree to keep them nullable. All governed by native PHP 7 nullable operator.
Of course, this can be changed or extended to be more type-specific if that makes any sense.
Update: this answer may be obsolete, but the comments contain an interesting discussion.
#Robert's workaround is buggy; in this part:
foreach ($reflectedProperties as $property) {
!$property->isInitialized($this) ??
$property->getType()->allowsNull() ? $property->setValue($this, null) : null;
}
the ?? must be corrected to &&.
Moreover that's a misuse of the ternary conditional; just use a classic if:
foreach ($reflectedProperties as $property) {
if (!$property->isInitialized($this)
&& $property->getType()->allowsNull()
) {
$property->setValue($this, null);
}
}
or:
foreach ($reflectedProperties as $property) {
if (!$property->isInitialized($this) && $property->getType()->allowsNull()) {
$property->setValue($this, null);
}
}
Probably not what you want, but you could use reflection:
<?php
class DragonBallClass
{
public string $goku;
public string $bulma = 'Bulma';
public string $vegeta = 'Vegeta';
}
$ob = new DragonBallClass;
$reflector = new ReflectionClass($ob);
foreach($reflector->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
echo $prop->name, ':', ($ob->{$prop->name} ?? 'NOT INITIALISED'), "\n";
}
Output:
goku:NOT INITIALISED
bulma:Bulma
vegeta:Vegeta
I have a bunch of optional settings and I'm sick of checking for isset and property_exists.
In Laravel, if I ask for a property that does not exist on a model or request, I get null and no complaints (errors). How can I do the same for my data structure.
If I try array, I can't do simple $settings['setting13'], I have to either pre-fill it all with nulls or do isset($settings['setting13']) ? $settings['setting13'] : '' or $settings['setting13'] ?? null. If I try an object (new \stdClass()), $settings->setting13 still gives me a warning of Undefined property.
How can I make a class such that it responds null or an empty string whenever it is asked for a property that it doesn't have?
Simply do what Laravel does, create a class that deals with your data structure which returns a value if key exists, and something else if it doesn't.
I'll illustrate with an example class (this class supports the "dot notation" of accessing array keys):
class MyConfigClass
{
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function get($path = '', $default = null)
{
if(!is_string($path))
{
return $default;
}
// There's a dot in the path, traverse the array
if(false !== strpos('.', $path))
{
// Find the segments delimited by dot
$segments = explode('.', $path);
$result = $this->data;
foreach($segments as $segment)
{
if(isset($result[$segment]))
{
// We have the segment
$result = $result[$segment];
}
else
{
// The segment isn't there, return default value
return $default;
}
}
return $result;
}
// The above didn't yield a result, check if the key exists in the array and if not - return default
return isset($this->data[$path]) ? $this->data[$path] : $default;
}
}
Use:
$my_structure = [
'url' => 'www.stackoverflow.com',
'questions' => [
'title' => 'this is test title'
]
];
$config = new MyConfigClass($my_structure);
echo $config->get('url'); // echoes www.stackoverflow.com
echo $config->get('questions.title'); // echoes this is test title
echo $config->get('bad key that is not there'); // returns null
There is also a possibility to create wrapper as Jon Stirling mentioned in a comments. This approach will allow to keep code clean and also add functionality via inheritance.
<?php
class myArray implements ArrayAccess {
private $container;
function __construct($myArray){
$this->container = $myArray;
}
public function offsetSet($offset, $value) {
if (is_null($offset)) {
$this->container[] = $value;
} else {
$this->container[$offset] = $value;
}
}
public function offsetExists($offset) {
return isset($this->container[$offset]);
}
public function offsetUnset($offset) {
unset($this->container[$offset]);
}
public function offsetGet($offset) {
return isset($this->container[$offset]) ? $this->container[$offset] : null;
}
}
$settings = array("setting1"=>1,"setting2"=>2,"setting3"=>3);
$arr = new myArray($settings);
echo $arr['setting1'];
echo "<br>";
echo $arr['setting3'];
echo "<br>";
echo $arr['setting2'];
echo "<br>";
echo "------";
echo "<br>";
echo $arr['setting4'] ?:"Value is null";
!empty($settings['setting13']) ? $settings['setting13'] : ''
can be replaced with
$settings['setting13'] ?: ''
as long as whatever you want to print and whatever you want to check exists is the same expression. It's not the cleanest thing ever - which would be to check the existence of anything - but it's reasonably clear and can be chained :
echo ($a ?: $b ?: $c ? $default ?: '');
However, you are not the first who are "sick of checking for isset and property_exists, it's just that we still have to do it, or else we get unexpected results when we expect it the least.
It's not about saving time typing code, it's about saving time not debugging.
EDIT : As pointed in the comments, I wrote the first line with isset() instead of !empty(). Since ?: returns the left operand if it's equal to true, it's of course uncompatible with unchecked variables, you have at least to check for existence beforehand. It's emptiness that can be tested.
The operator that returns its left operand if it exists and is different from NULL is ??, which can be chained the same way ?: does.
Admittedly not the best way to do this, but you can use the error suppressor in php like this:
$value = #$settings['setting13'];
This will quitely set$value to NULL if $settings['setting13'] is not set and not report the undefined variable notice.
As for objects, you should just calling for attributes that are not defined in class.
I'm using Laravel 4.2 PHP framework. This is so odd that the code is correct, but it's always return unexpected boolean value.
$enjoying = (DB::table('enjoy')->where('user_id','=',$authUserId)->where('video_id','=',$video_id)->get()) ? true : false;
I'm so sure that I have a table called "enjoy" where user_id and video_id is matches exactly the above statement. But it returns false always unexpectedly.
On the other hand, I also created a Enjoy Model, so I replace and modify the above statement.
$enjoying = (Enjoy::where('user_id','=',$authUserId)->where('video_id','=',$video_id)->get()) ? true : false;
This time again, assume my table 'enjoy' don't have any data. But it will unexpectedly returns TRUE always. I have no idea what's going on.
Enjoy Model
<?php
class Enjoy extends Eloquent
{
protected $fillable = array('user_id', 'video_id');
protected $table = 'enjoy';
public $timestamps = false;
public function users() {
return $this->has_many_and_belongs_to('User');
}
}
Try this...
$data = Enjoy::where('user_id','=',$authUserId)->where('video_id','=',$video_id)->get();
echo $enjoying = (count($data)) ? 1 : 0;
or
echo $enjoying = (count($data)) ? true : false;
Maybe you need the "exists" method?
$enjoying = Enjoy::where('user_id','=',$authUserId)
->where('video_id','=',$video_id)
->exists();
I'm currently working with the medoo.php framework, and although I would normally use their ticket area on github, it appears that no one actually uses that... so...
At any rate, when I'm running one of my files which uses "require" to call the framework, I get the following error:
Warning: Cannot use a scalar value as an array in /home/..../public_html/projects/friendcodes/medoo.min.php on line 759
However, when I inspect the code (the below is lines 752 to 764), I see that it is in fact supposed to check if $where is not set, and if it isn't, make it an array - however this php error begs to differ.
I'm guessing that $where is being set as a variable somewhere else, that's not an array, but there are over 100 occurrences of the variable in the framework, and 830 lines of code, which you probably don't want to see. (Let me know in a comment and I'll add it - again, this is directly from medoo's most two recent updates/releases.)
public function get($table, $columns, $where = null)
{
if (!isset($where))
{
$where = array();
}
$where['LIMIT'] = 1;
$data = $this->select($table, $columns, $where);
return isset($data[0]) ? $data[0] : false;
}
My main question is - How do I rectify this problem without breaking something in this framework which is extremely complex (for my level, at any rate)
Update: How silly of me! I found the problem. Just as people suggested, I was calling $where wrong.
I was calling it with:
$accountinfo = $database->get('xf_user_field_value', ['field_value'], 1);
Instead of
$accountinfo = $database->get('xf_user_field_value', ['field_value'], ["user_id"=>1]);
(Where the third arg is $where) Thanks for the help guys!
Right, first things first, we need to find out what is calling get that shouldn't be. WHICH IS THE ENTIRE PROBLEM. The problem isn't the function itself, the problem is something is calling it using an argument for $where which isn't an array. Changing a library to fix one faulty call is ridiculous.
Step 1: Temporarily edit the get function to include a print_r of the $where variable.
public function get($table, $columns, $where = null)
{
if(isset($where)) print_r($where);
if (!isset($where))
{
$where = array();
}
$where['LIMIT'] = 1;
$data = $this->select($table, $columns, $where);
return isset($data[0]) ? $data[0] : false;
}
This will show us before the error prints the value of $where, which will help you find the malformed get call.
If this fails, try using PHP's built-in backtrace to try to find the issue:
public function get($table, $columns, $where = null)
{
if(isset($where)) print_r(debug_backtrace());
if (!isset($where))
{
$where = array();
}
$where['LIMIT'] = 1;
$data = $this->select($table, $columns, $where);
return isset($data[0]) ? $data[0] : false;
}
The ->get() method is not called properly.
Cannot use a scalar value as an array
That warning is shown if $where is either true, a numeric value or a resource. Valid method calls include:
->get('table', '*')
->get('table', '*', array('WHERE' => 'foo = "bar"'))
Check the manual and fix your code.
EDIT 3: try moving $where['LIMIT'] = 1; inside of the isset statement, since you wouldn't want to pass LIMIT 1 to the query constructor if $where is passed by reference.
DISCLAIMER I have no knowledge of the medoo framework.
public function get($table, $columns, $where = null)
{
if (is_null($where))
{
$where = array('LIMIT'=>1);
}
$data = $this->select($table, $columns, $where);
return isset($data[0]) ? $data[0] : false;
}
Right now I'm trying to write a function that would allow me to access member functions. The code in question looks a little like this:
protected $formName;
protected $formClass;
protected $formAction;
protected $formMethod;
protected $formObjArray = array(); //outputs in order. So far it should only take newLine, selectTag, inputTag, textTag.
protected $submitBtnVal;
protected $encType;
function __construct($args) {
$this->formName = $args['formName'];
$this->formAction = $args['formAction'];
if (isset($args['formClass'])) $this->formClass = $args['formClass'];
if (isset($args['encType'])) $this->encType = $args['encType'];
//default should be POST. Hell, you should never really be using GET for this..
//also, the default submit value is Submit
$this->formMethod = isset($args['formMethod']) ? $args['formMethod'] : "POST";
$this->submitBtnVal = isset($args['submitBtnVal']) ? $args['submitBtnVal'] : "Submit";
}
//get functions
function getFormName () { return $this->formName; }
function getFormAction () { return $this->formAction; }
function getFormMethod () { return $this->formMethod; }
function getSubmitBtnVal () { return $this->submitBtnVal; }
function getEncType () { return $this->encType; }
//set functions
function setFormName ($newName) { $this->fromName = $newName; }
function setFormAction ($newAction) { $this->formAction = $newAction; }
function setFormMethod ($newMethod) { $this->formMethod = $newMethod; }
function setEncType ($newEType) { $this->encType = $newEType; }
function addTag($newTag) {
if ($newTag instanceof formTag || $newTag instanceof fieldSetCont || $newTag instanceof newLine
|| $newTag instanceof noteTag)
$this->formObjArray[] = $newTag;
else throw new Exception ("You did not add a compatible tag.");
}
I'd like to be able to call $myForm->getTagByName("nameA")->setRequired(true);
How would I do that? Or would I need to do something more like..
$tagID = $myForm->getTagByName("nameA");
$myForm->tagArray(tagID)->setRequired(true);
Nothing in your code seems to be protected so you should have no trouble accessing any of it.
It looks like all your tags are in $formObjArray so it should be trivial to filter than array and return tags that match the name you've passed in. The trouble you will have is that, getTagByName really should be getTagsByName and should return an array because you can have more than one tag with the same name. Since it will return an array, you can not call setRequired on the return value, arrays don't have such a method. You'll need to do it more like:
$tags = $myForm->getTagsByName("nameA");
foreach ($tags as $tag) {
$tag->setRequired(true);
}
Exactly what are you stuck on? Maybe I don't understand the question very well.
So maybe the filtering has you stuck? Try this (if you you're using at least php 5.3)
function getTagsByName($tagname)
{
return array_filter($this->formObjArray, function($tag) use($tagname) {
return $tag->getName() == $tagname;
});
}
No ifs or switches.
Prior to 5.3, you don't have lambda functions so you need to do it differently. There are several options but this may be the simplest to understand:
function getTagsByName($tagname)
{
$out = array();
foreach ($this->formObjArray as &$tag) {
if ($tag->getName() == $tagname) {
$out[] = $tag;
}
}
return $out;
}
In your addTag method, you are storing new tags in $this->formObjArray using the [] notation, which will just append the new tag to the end of the array. If your tag objects all have a getName() method, then you can do something like this:
$this->formObjArray[$newTag->getName()] = $newTag;
Then, you can easily add a getTagByName() method:
public function getTagByName($name) {
if (array_key_exists($name, $this->formObjArray) {
return $this->formObjArray($name);
}
else {
return null;
}
}
Please beware of the solutions suggesting you to iterate through all the tags in your array! This could become very costly as your form gets larger.
If you need to use the [] construct because the order of the elements added is important, then you can still maintain a separate index by name, $this->tagIndex, that will be an associative array of name => tag. Since you are storing object references, they will not be using much space. Assuming that getTagByName will be used many times, this will save you a lot of resources over iterating the tags array on every call to getTagByName.
In that case, your addTag method would look like this:
$this->formObjArray[] = $newTag;
$this->tagIndex[$newTag->getName()] = $newTag; // it seems that you're doubling the memory needed, but you're only storing object references so this is safe
EDIT : Here is some modified code to account for the fact that multiple tags can have the same name:
In your addTag() method, do:
$this->formObjArray[] = $newTag;
$tag_name = $newTag->getName();
if (!array_key_exists($tag_name, $this->tagIndex)) {
$this->tagIndex[$tag_name] = array();
}
$this->tagIndex[$tag_name][] = $newTag
You can then rename getTagByName to getTagsByName and get the expected result.
As mentioned in the comments, this is only useful if you will call getTagsByName multiple times. You are trading a little additional memory usage in order to get quicker lookups by name.