Lets say that I have a View that uses 3 variables, while developing on Laravel 4:
<ul>
<li>Name: {{name}}</li>
<li>Surname: {{surname}}</li>
<li>Age: {{age}}</li>
<ul>
In my controller, I could be adding the data to the view the following way.
$context = array('name' => $name, 'surname' => $surname, 'age' => $age);
return View::make('myview');
But this code isnt completely DRY, since I am writing twice the name of the variable. It would be optimal if I could do:
$context = array($name, $surname, $age);
return View::make('myview', $context);
This code seems tighter. But unfortunately, the view will give me an error "Undefined variable: name".
Is there any way to achieve this with Laravel?
UPDATE: Another option, as stated in some answers is using compact.
$context = compact('name', 'surname', 'age')
return View::make('myview', $context);
But the problem here is that you have to create all your context variable in one go. And programming is not always so convenient sadly. Sometimes you will want to be adding your elements to the context, as you go progressing through the function.
For instance:
$context = array($name, $surname, $age);
if ($age > 18){
$adult = "He is adult";
array_push($context, $adult);
}
return View::make('myview', $context);
So as you see, if I could use arrays, it wouldnt be a problem because I could push new elements into the $context with array_push. But I can not use arrays that way, unfortunately. And compact wouldn't allow me to push elements inside the context, would it?
You use compact(), a native PHP function (see docs):
compact — Create array containing variables and their values
For each of these, compact() looks for a variable with that name in the current symbol table and adds it to the output array such that the variable name becomes the key and the contents of the variable become the value for that key. In short, it does the opposite of extract().
Any strings that are not set will simply be skipped.
For example, to create an array with keys of name, surname, age, with their values respectively:
$name = 'foo';
$surname = 'bar';
$age = 99;
$context = compact("name", "surname", "age");
return View::make('myview', $context);
If you var_dump($context) you would get:
array(3) {
["name"] => string(3) "foo"
["surname"] => string(3) "bar"
["age"] => int(99)
}
Which is what you want to pass into View::make().
Update:
To respond the part of question about using array_push(). I don't think you can use array_push() regardless of compact() anyway. The View::make() still requires a two-dimensional array with key => values. Say you have this array:
$context = array(
'name' => 'foo',
'surname' => 'bar',
'age' => 99,
);
Doing an array_push($context, $adult) would give you:
array(4) {
["name"] => string(3) "foo"
["surname"] => string(3) "bar"
["age"] => int(99)
[0] => string(11) "He is adult"
}
You get [0] => string(11) "He is adult" instead of ["adult"] => string(11) "He is adult". You would not get your expected result in your view using array_push()
To answer "Sometimes you will want to be adding your elements to the context, as you go progressing through the function.", I say there are two ways:
1st approach: Append $context as you go. Similar to your example:
$context = compact("name", "surname", "age");
if ($age > 18) {
$context['adult'] = "He is adult"; // You need to do this way because array_push() does not support setting your own key.
}
return View::make('myview', $context);
Pros: No need to worry whether you have added your variable to the compact() yet.
Cons: To know what is being passed to your view, you need to scan your whole method for key/value that you appended to $context.
2nd approach: Prepare all data then compact them into $context only right before sending to the view.
$name = 'foo';
$surname = 'bar';
$age = 99;
if ($age > 18){
$adult = "He is adult";
}
$context = compact("name", "surname", "age", "adult");
return View::make('myview', $context);
Pros: Single point of compiling view context. You can easily see all variables that are compacted and sent to the view.
Cons: Misspellings are harder to detect. compact() can become really long if you are passing a lot of variables.
Related
In CoffeeScript, Clojure, ES6 and many other languages we have destructuring of objects/maps/etc somewhat like this:
obj = {keyA: 'Hello from A', keyB: 'Hello from B'}
{keyA, keyB} = obj
I've found the list function in php which lets you destructure arrays like so:
$info = array('coffee', 'brown', 'caffeine');
list($drink, $color, $power) = $info;
Is there a way to destructure objects or associative arrays in PHP? If not in the core libs maybe someone wrote some smart helper function?
For PHP 7.0 and below that is beyond the functionality of list. The docs state:
list only works on numerical arrays and assumes the numerical indices start at 0.
One of the things that could suit your purpose would be the extract() function which imports variables from an array into the current symbol table. While with list you are able to define variable names explicitly, extract() does not give you this freedom.
Extracting an associative array
With extract you could do something like that:
<?php
$info = [ 'drink' => 'coffee', 'color' => 'brown', 'power' => 'caffeine' ];
extract($info);
var_dump($drink); // string(6) "coffee"
var_dump($color); // string(5) "brown"
var_dump($power); // string(8) "caffeine"
Extracting an Object
Extracting an object works almost the same. Since extract only takes an array as an argument we need to get the objects properties as an array. get_object_vars does that for you. It returns an associative array with all public properties as key and their values as value.
<?php
class User {
public $name = 'Thomas';
}
$user = new User();
extract( get_object_vars($user) );
var_dump($name); // string(6) "Thomas"
Pitfalls
extract() is not the same as list since it does not allow you to explicitly define the variable names that get exported to the symbol table. The variable names correspond the array keys by default.
list is a language construct while extract() is a function
It might happen that you overwrite variables that you have defined beforehand unintentionally
Your array keys might be invalid as variable names
With the $flags parameter that you can pass as second argument to extract() you can influence the behavior in case of colliding or invalid variables. But still it's important to know how extract() works and to use it with cauton.
Edit: As of PHP 7.1 this is possible:
http://php.net/manual/en/migration71.new-features.php#migration71.new-features.support-for-keys-in-list
You can now specify keys in list(), or its new shorthand [] syntax. This enables destructuring of arrays with non-integer or non-sequential keys.
https://php.net/manual/en/migration71.new-features.php#migration71.new-features.symmetric-array-destructuring
The shorthand array syntax ([]) may now be used to destructure arrays for assignments (including within foreach), as an alternative to the existing list() syntax, which is still supported.
For example this:
$test_arr = ['a' => 1, 'b' => 2];
list('a' => $a, 'b' => $b) = $test_arr;
var_dump($a);
var_dump($b);
Will output the following as of 7.1.0
int(1)
int(2)
I noticed the accepted answer missed out examples that use the short-hand notation, security issues with using extract, and IDE issues.
Numerical Array Destructuring (PHP 7.1)
As of PHP 7.1 numerical array destructuring (Symetric array destructuring) is supported like so:
<?php
$data = [55, 'John', 'UK'];
[$id, $name] = $data; // short-hand (recommended)
list($id, $name) = $data; // long-hand
Notice that you can miss items out if you don't want them.
Associative Array Destructuring (PHP 7.1)
You can also destructure associative arrays (Support for keys in list) like so:
<?php
$data = ['id' => 55, 'firstName' => 'John', 'country' => 'UK']
['id' => $id, 'firstName' => $name] = $data; // short-hand (recommended)
list('id' => $id, 'firstName' => $name) = $data; // long-hand
Notice that you can miss items out if you don't want them. Also the variable name can be different to the property name.
Object Destructuring (PHP 7.1)
Unfortunately there is no object destructuring. However you can convert an object to an associative array using get_object_vars, and then use associative array destructuring.
<?php
class User {
public $id;
public $name;
public $country;
}
$user = new User();
$user->id = 55;
$user->name = 'John';
$user->country = 'UK';
['id' => $id, 'firstName' => $name] = get_object_vars($user)
However, this can break some IDE features. These are some issues I noticed when using PHPStorm 2019.1:
IDE's may no longer understand the type for the variables, so you would need to add some #var Type PHPDocs to maintain auto-complete functionality
Does not work well with refactoring tools. For example, if you rename one of the properties, the array destructuring portion will not also automatically rename.
So I recommend just doing it the normal way:
$id = $user->id
$name = $user->firstName
Do NOT use extract
With extract, all variables are always set. There it is a really bad idea to use it because:
It can lead to security issues. Even if your careful, it can lead to non-obvious security holes in the future. If you do use it, don't use it with user input (e.g. $_GET, $_POST), unless you want to make a malicious hacker's day.
Can lead to hard to detect bugs
If the class or array changes in the future, by introducing new properties, it can break your code if it coincides with an already used variable, unless you use the EXTR_SKIP flag or similar
Variable variables are one way to achieve this:
$args = ['a' => 1, 'b' => 2, 'c' => 3];
foreach (['a', 'c'] as $v) $$v = $args[$v];
// $a is 1, $b is undefined, $c is 3
It's really not pretty, and thankfully this has been addressed in 7.1 by https://wiki.php.net/rfc/short_list_syntax . This would let you say ['a' => $a, 'c' => $c] = $args; in the above example.
Since 7.1 includes a way to use a different name for your var than the assoc array key. This is pretty straight-forward using variable variables here too:
foreach (['a' => 'eh', 'b' => 'bee'] as $k => $v) $$v = $args[$k];
// $eh is 1, $bee is 2
Some developers, and some coding styles, define $$var as an anti-pattern similar to using eval, extract, and the GPR magic variables directly. This is because using variable variables makes code harder to understand, which leads directly to bugs and prevents static code analysis tools from functioning.
If you do adopt $$var, it can be helpful to use the ${$var} form instead, which makes it obvious that the author didn't simply type one too many $'s, and may spare the author immediate negative feedback when their code is audited.
One simple solution is to read the Object as an Array. So assuming you use #Yahya Uddin's User object, you can do like so:
['id' => $id, 'firstName' => $name] = (array)$user
// $id = 55, name = 'john'
This will tell PHP to read this objective as an associative Array.
I have two PHP arrays in a section of a website I'm building for a class. One contains data that I created with a simple foobar array bit, and the other is created from a fetchAll() PDO call. I can't access the PDO array's element for some reason, so I created a sample array and called var_export() on both arrays to see if there were any differences. Here is the code:
if ($dao->userExistsLogin($_POST["email"], $_POST["password"]))
{
$useremail = $_POST["email"];
$_SESSION["authenticated"] = "true";
$logger->LogDebug("email from post array in login handler: " . $_POST["email"]);
$user_results = $dao->getIDFromEmail($useremail);
$logger->LogDebug(print_r($user_results, true));
$array = [
"foo" => "bar",
"bar" => "foo",
];
$logger->LogDebug(var_export($array, true));
$logger->LogDebug(var_export($user_results, true));
$logger->LogDebug(gettype($user_results));
$_SESSION['user_id'] = $user_results['id'];
header("Location: http://localhost/WIWO/user_account.php");
}
The GetIDFromEmail function is here:
public function getIDFromEmail($email)
{
$conn = $this->getConnection();
$getIDFromEmailQuery = "SELECT * FROM user WHERE email=:email";
$query = $conn->prepare($getIDFromEmailQuery);
$query->bindParam(":email", $email);
$query->execute();
$result = $query->fetchAll(PDO::FETCH_ASSOC);
$this->logger->LogDebug(print_r($result, true));
return $result;
}
And here's what's in my logger from the var_export() commands:
2020-10-31 17:57:10 - DEBUG --> array (
'foo' => 'bar',
'bar' => 'foo',
)
2020-10-31 17:57:10 - DEBUG --> array (
0 =>
array (
'id' => '251',
'email' => 'heck',
'institution' => 'heck',
'access' => '1',
),
)
Why can I not access the second array at $_SESSION['user_id'] = $user_results['id'];? For some reason that line totally fails to do anything. I know the session is started and correct because other variables stored in the SESSION superglobal are showing up correctly, but the user_id never gets set. Does it have something to do with "0 =>" showing up in the var_export() of the second array? I notice that doesn't show up in the first array - is the second array malformed or something?
You're using PDOStatement::fetchAll(), which will always return an array of rows. Instead you might want to use PDOStatement::fetch() which will return precisely one row.
I haven't seen the userExistsLogin() method, but you could probably simplify your code if you have that method return the user id on success, and something falsey (e.g. 0, FALSE or NULL) on error. This will not only reduce code complexity, but it will reduce database load as well.
I'm stuck with a very weird bug. I have an object called $row that looks like this:
stdClass Object
(
[title] => Some Title
[body] => My body
[topic] => Topic
[dataType] => Survey
[csvrownum] => 1
)
I'm just trying to print out the title property in the following way:
print_r($row->title);
However for some reason that doesn't output anything.
Then I've tried to manually set the title property and print it right after, something like this:
$row->title = 'My Title';
print_r($row->title);
Surprisingly it worked but why? To make this more strange I decided to var_dump the object after set the title variable by hand:
$row->title = 'My Title';
var_dump($row);
And this is what I've got:
class stdClass#391 (6) {
public $title =>
string(3) "Some title"
public $body =>
string(7) "My body"
public $topic =>
string(6) "Topic"
public $dataType =>
string(17) "Survey"
public $csvrownum =>
int(1)
public $title =>
string(8) "My title"
}
Notice the title key is duplicated with different values. Is there any condition under this could happen?
No, PHP does not allow an object to have duplicate property names, because objects in PHP are implemented just like arrays. They are both implemented as ordered hashmaps. In a hashmap, two things that have the same hash, overwrite each other.
You likely just have unprintible characters in your object property name. You can see this more clearly by doing something like the following for debug purposes...
foreach($row as $key => $value) {
var_dump($key);
}
If we had an object like this, for example, you'd see it gets overwritten.
$row = new stdClass;
$row->title = "First";
$row->title = "Second";
But something like this might be more deceptive...
$row = new stdClass;
$row->{"title\0"} = "First";
$row->title = "Second";
Output from the foreach using var_dump on the key, would reveal this...
string(6) "title"
string(5) "title"
Notice one is string of length 6 and the other is a string of length 5.
Grain of salt
It's always better to use var_dump when attempting to debug variables than using something like print_r, as var_dump was specifically designed for debug purposes, whereas print_r is just a recursive print (hence the name). Printing values like null, false, or empty strings, gives you no useful information for debug purposes, but var_dump does.
Given the following array:
$array = array(
'item_1' => array(
'item_1_1' => array(
'item_1_1_1' => 'Hello',
),
'item_1_2' => 'World',
),
'item_2' => array(),
);
How can I convert that into an Object?
Option 1
$obj = (object) $array;
Or
Option 2
$object = json_decode(json_encode($array), FALSE);
Or something else?
I would like to know the difference in the output between the 2 option and understand the best practice for creating this conversion.
Well you are answering somehow your own question, but if you want to have an object with the attributes like your array you have to cast it, this way an array will remain an array
$obj = (object) $array;
OUTPUT:
object(stdClass)#1 (2) {
["item_1"]=>
array(2) {
["item_1_1"]=>
array(1) {
["item_1_1_1"]=>
string(5) "Hello"
}
["item_1_2"]=>
string(5) "World"
}
["item_2"]=>
array(0) {
}
}
if you are using the json_decode version it will convert arrays to objects too:
object(stdClass)#2 (2) {
["item_1"]=>
object(stdClass)#3 (2) {
["item_1_1"]=>
object(stdClass)#4 (1) {
["item_1_1_1"]=>
string(5) "Hello"
}
["item_1_2"]=>
string(5) "World"
}
["item_2"]=>
array(0) {
}
}
NOTE: just the empty array will be an array here.
To Answer your question: The best practice depends on what YOU need.
It depends, really: if you are working on data that might be an array in one case, and an object the next, it would probably be best to use the json_decode trick, simply because unlike a cast, its result is "recursive". There is one very important thing to keep in mind here, though: numeric indexes can, and probably will cause problems for you at some point in time. Take a look at this bug report
This is documented here, but not in a way that really stands out:
If an object is converted to an array, the result is an array whose elements are the object's properties. The keys are the member variable names, with a few notable exceptions: integer properties are unaccessible;
Exampe of the problem:
$data = [
'foo' => 'bar',
123 => 'all is well',
];
$obj = json_decode(json_encode($data));
var_dump($obj->foo);//bar
var_dump($obj->{123});//all is well
$cast = (array) $obj;
var_dump($cast);//shows both keys
var_dump(isset($cast[123]));//FALSE!!!
var_dump(isset($cast['123']));//FALSE
Basically: If you start converting arrays to objects and back again, numeric keys are not reliable anymore. If I were you, I'd simply change the code that is passing the data where possible, or I'd create a value object that can be set using an array or an object, and normalize the data that way.
Basically how I understand references work is
$a = 5;
$b = &$a;
$a = 10;
echo $b; // 10;
However in this bit of code I'm getting unexpected (for me, which probably has an explanation) result
class Room {
private $users = array();
public function addUser(&$user){
$this->users[] = $user;
}
}
$users = array(
1 => 'Tom',
2 => 'Hank',
3 => 'Sam',
4 => 'John'
);
$room = new Room();
$room->addUser($users[1]);
$room->addUser($users[3]);
unset($users[3]);
echo "<pre>" . print_r($room, true) . "</pre>";
echo "<pre>" . print_r($users, true) . "</pre>";
I expect, after unsetting $users[3], the only user inside of $room to be Tom, but that is not the case, both Tom and Sam are present in the object. Why is unset not affecting the object's property?
EDIT:
Even if I take things a step further with the example and create a class User the effect is still the same
class Room {
private $users = array();
public function addUser(&$user){
$this->users[] = $user;
}
}
class User {
public $name;
function __construct($name){
$this->name = $name;
}
}
$users = array(
1 => new User('Tom'),
2 => new User('Hank'),
3 => new User('Sam'),
4 => new User('John')
);
$room = new Room();
$room->addUser($users[1]);
$room->addUser($users[3]);
unset($users[3]);
echo "<pre>" . print_r($room, true) . "</pre>";
echo "<pre>" . print_r($users, true) . "</pre>";
Unset operates on symbols, not reference targets.
That is why using unset on an undefined variable doesn't raise any kind of error.
$a = 10;
$b = &$a;
unset($b); // forget the name "$b" exists.
echo $a; // 10
If you want to unset it in both places, you have to assign null to one of the variables. This is a "hard unset", as opposed to a "soft unset" which is what you are currently doing.
Also you are not assigning a reference, you're assigning a copy.
$this->users[] = &$user;
Reference Counting Basics :
A PHP variable is stored in a container called a "zval". A zval
container contains, besides the variable's type and value, two
additional bits of information. The first is called "is_ref" and is a
boolean value indicating whether or not the variable is part of a
"reference set". (...) Since PHP allows user-land references, as
created by the & operator, a zval container also has an internal
reference counting mechanism to optimize memory usage. This second
piece of additional information, called "refcount", contains how many
variable names (also called symbols) point to this one zval container.
(...)
Variable containers get destroyed when the "refcount" reaches zero.
The "refcount" gets decreased by one when any symbol linked to the
variable container leaves the scope (e.g. when the function ends) or
when unset() is called on a symbol.
Example with arrays:
<?php
$a = array(
0 => 'aaa',
1 => 'bbb',
2 => 'ccc',
);
debug_zval_dump($a);
// ... string(3) "bbb" refcount(1) ...
$b = array();
$b[0] = &$a[0];
$b[1] = &$a[1];
$a[1] = 'ddd';
debug_zval_dump($a);
// ... &string(3) "bbb" refcount(2) ...
debug_zval_dump($b);
// ... &string(3) "bbb" refcount(2) ...
unset($a[1]);
debug_zval_dump($a);
/*
array(2) refcount(2){
[0]=>
&string(3) "aaa" refcount(2)
[1]=>
&string(3) "ddd" refcount(2)
}
*/
debug_zval_dump($b);
// ... string(3) "ddd" refcount(1) ...
var_dump($a);
/*
array (size=2)
0 => &string 'aaa' (length=3)
2 => string 'ccc' (length=3)
*/
var_dump($b);
/*
array (size=2)
0 => &string 'aaa' (length=3)
1 => string 'ddd' (length=3)
*/
I think there's a slight logical problem between your desired effect and the way you try to do it.
If I understand correctly, you want to assign users to a container, then unsetting one of those user in a way that it will also be unsetted in your container. This
unset($users[3]);
unsets the value of the fourth element of your users array.
if we did $user[3] = 'foo'; the value contained in the corresponding container's entry will be set to 'foo' as well, but the container's index key itself will not get unset, or affected by the reference, because it is not part of the referenced value
If you want to unset the user, either you keep track of which index key is assigned to which user in your container and then delete users with this index key, or you set the value of $users[3] to null (or whatever suits your needs) and skip the null values when dealing with your container
You can change a value of arrays, like this:
CODE:
private $users = array();
public function addUser(&$user){
$this->users[] = &$user;
}
}
$users = array(
1 => 'Tom',
2 => 'Hank',
3 => 'Sam',
4 => 'John'
);
$room = new Room();
$room->addUser($users[1]);
$room->addUser($users[3]);
$users[3] = "AAA123";
echo "<pre>" . print_r($room, true) . "</pre>";
echo "<pre>" . print_r($users, true) . "</pre>";
OUTPUT:
Room Object
(
[users:Room:private] => Array
(
[0] => Tom
[1] => AAA123
)
)
Array
(
[1] => Tom
[2] => Hank
[3] => AAA123
[4] => John
)
But delete it's not possible this way... I don't know how to explain, so just give example:
$a = 10;
$b = &$a;
unset($a);
echo $b; // 10
Then you deleting variable name, you not delete zval(container), until refcount reach 0... then "Garbage Collection" do all work and delete zval...
So method unset() remove variable name only in this case...
Be careful.
You are passing to addUser() a reference to the string 'Tom' allocated while building the array $users.
First, addUser() should read $this->users[] =& $user;, otherwise you will be copying the value into $this->users[] instead of sharing the reference.
Now, both $users and Room::$users share the same objects, however unset($users[3]) removes the element mapped by the index 3 from the array, it does not destroy the mapped object.