phpstan - Validate an array which can be mixed - php

I am blocked with some phpstan validation, I have this array:
/** #var array<string, string|array<string>> $normalizedImage */
$normalizedImage = $this->normalizer->normalize($data);
$normalizedImage['#id'] = $this->iriConverter->getIriFromItem($data);
$normalizedImage['property']['#id'] = $this->iriConverter->getIriFromItem($object);
The error is:
phpstan: Cannot assign offset '#id' to array|string.
I tried most of combinations in the comment, but I can't figure out what to put here.

Looking at the phpdoc
#psalm-return array{_labels?: array<string>}&array<int|string>
#phpstan-return array<int|string|array<string>>
I would say it's not surprising, since phpstan doesn't support a type similar to set both specific keys types and generics ones. But this would be great.
Can be fixed this way :
<?php declare(strict_types = 1);
$result = [
'#id' => [],
];
$labels = [];
foreach ([1, 2, 3] as $id) {
$result[] = $id;
$labels[] = 'asda';
}
$result['#id'] = $labels;
You can try it here: https://phpstan.org/try
There is no errors anymore

Related

PHP, Static Analysis, and Recursive Type Checking

I'm looking at a Database ORM that uses an array to define the WHERE clause, e.g.
$articles->find('all', [
'OR' => [
'category_id IS NULL',
'category_id' => $id,
],
]);
The "array keys" become part of the SQL, so they must be a developer defined string (aka a literal-string), otherwise you can have mistakes like this:
$articles->find('all', [
'OR' => [
'category_id IS NULL',
'category_id = ' . $id, // INSECURE, SQLi
],
]);
If the "array values" simply contained the values to be parameterised (i.e. user values), then I could specify the parameter type as array<literal-string, int|string>.
But, as you will notice with the 'OR' key, the parameter can contain nested arrays, and can be many levels deep.
Is it possible to get Static Analysis tools like Psalm or PHPStan to work with this?
I can use the CakePHP implementation as an example of how this works:
<?php
class orm {
/**
* #param array<int, literal-string|array<mixed>>|array<literal-string, int|string|array<mixed>> $conditions
*/
public function find(string $finder, array $conditions): void {
print_r($this->_addConditions($conditions));
}
/**
* #param array<int, literal-string|array<mixed>>|array<literal-string, int|string|array<mixed>> $conditions
* #param literal-string $conjunction
* #return array{literal-string, array<int, mixed>}
*/
private function _addConditions(array $conditions, string $conjunction = 'AND'): array {
// https://github.com/cakephp/cakephp/blob/ab052da10dc5ceb2444c29aef838d10844fe5995/src/Database/Expression/QueryExpression.php#L654
$operators = ['and', 'or', 'xor'];
$sql = [];
$parameters = [];
foreach ($conditions as $k => $c) {
if (is_numeric($k)) {
if (is_array($c)) {
/** #var array<int, array<mixed>> $sub_conditions */
$sub_conditions = $c;
list($new_sql, $new_parameters) = $this->_addConditions($sub_conditions, 'AND');
$sql[] = $new_sql;
$parameters = array_merge($parameters, $new_parameters);
} else if (is_string($c)) {
$sql[] = $c; // $c must be a literal-string
}
} else {
$operatorId = array_search(strtolower($k), $operators);
if ($operatorId !== false) {
/** #var array<literal-string, int|string|array<mixed>> $sub_conditions */
$sub_conditions = $c;
list($new_sql, $new_parameters) = $this->_addConditions($sub_conditions, $operators[$operatorId]);
$sql[] = $new_sql;
$parameters = array_merge($parameters, $new_parameters);
} else {
$sql[] = $k . ' = ?'; // $k must be a literal-string
$parameters[] = $c;
}
}
}
/** #var literal-string $sql */
$sql = '(' . implode(' ' . $conjunction . ' ', $sql) . ')';
return [$sql, $parameters];
}
}
$articles = new orm();
?>
PHPStan
"Recursive types aren't supported now and I don't know if they ever will" Mar 2021.
"PHPStan doesn't support recursive types as they're very hard to do" May 2022.
Psalm
"Recursive types aren’t supported by Psalm (and probably won’t ever be tbh)" Jul 2019.
"Yeah, you can't - if you search issues you'll see a few requests for recursive types that have come up (and I've dismissed)" Feb 2020.

PHP - Get array reference by his string name

If I do this:
$array = ["one", "two", "three"];
$array_copy = "$array"; // or "{$array}"
I doesn't get the original array but instead a string conversion of it. ¿Is there any way to acomplish this task? To get the array reference by his string name.
Thank you.
Edit:
I am aware of this:
$array = ["one", "two", "three"];
$array_copy = $"array";
or
$name = "array";
$array_copy = $$name
But I need to achive this in any situation. Example:
$array = ["one", "two", "three" => ["four"] ];
$sub_array = "{$array['three']}"; // this returns an string, not the array ["four"]
I hope is more clear now.
Edit 2
Let's put it in other way. Imagine you need that an user input (string) be able to access the content of any declared variable. Example:
$customer = [ "name"=>"Peter", "address"=> ["street"=>"5th", "number"=>1969] ];
$variable_name = $_GET["varname"];
var_export( $$variable_name ); // What can write the user to print $customer["address"] array?
because you equal to string, just do it directly
$array_copy = $array
but copy is just copy, not a referense, if you want reference you should write like this
$array_copy = &$array
but there are should be reasons to get the reference
or if you have some variable with array name then you can do like this
$array = ["one", "two", "three"];
$arrayName = 'array';
$array_copy = $$arrayName;
You may use a function that takes a path such as customer.address as a parameter to retrieve the address index of the $customer array automatically:
$customer = ['name' => 'Peter', 'address' => ['street' => '5th', 'number' => 1969]];
/**
* #param array $array
* #param string $path A dot-separated property path.
* #param mixed $default
* #return mixed
*/
function getArrayValue(array $array, string $path, $default = null)
{
$parts = explode('.', $path);
return array_reduce($parts, static function ($value, $part) use ($default) {
return $value[$part] ?? $default;
}, $array);
}
/**
* #param string $path A dot-separated path, whose first part is a var name available in the global scope.
* #param mixed $default
* #return mixed
*/
function getGlobalArrayValue(string $path, $default = null)
{
#list($varName, $propertyPath) = explode('.', $path, 2);
return getArrayValue($GLOBALS[$varName] ?? [], $propertyPath, $default);
}
echo getGlobalArrayValue('customer.name'), PHP_EOL; // Peter
echo getGlobalArrayValue('customer.address.street'), PHP_EOL; // '5th'
echo getGlobalArrayValue('customer.address.idontexist', 'somedefaultvalue'), PHP_EOL; // 'somedefaultvalue'
echo getGlobalArrayValue('idontexist.address', 12); // 12
Demo: https://3v4l.org/O6h2P

Getting rid of ?: operator in PHP

I have a problem with PHP code for survey. The code only takes one value, "answer" and if answer is not available it takes "otherAnswer". I need it to take both "answer" and "otherAnswer"
Here is the code. Thanks for help.
protected function convertRequestToUserAnswersArray()
{
$answers = [];
if ($this->request->hasArgument('answers')) {
/** #noinspection PhpUnhandledExceptionInspection */
$requestAnswers = $this->request->getArgument('answers');
/** #noinspection PhpWrongForeachArgumentTypeInspection */
foreach ($requestAnswers as $questionUid => $requestAnswer) {
$answers[$questionUid] = $requestAnswer['answer'] ?: $requestAnswer['otherAnswer'];
}
}
return $answers;
}
Try to append both answers. If you want both answers you don't need to use ternary operator
protected function convertRequestToUserAnswersArray()
{
$answers = [];
if ($this->request->hasArgument('answers')) {
/** #noinspection PhpUnhandledExceptionInspection */
$requestAnswers = $this->request->getArgument('answers');
/** #noinspection PhpWrongForeachArgumentTypeInspection */
foreach ($requestAnswers as $questionUid => $requestAnswer) {
$answers[$questionUid] = $requestAnswer['answer'] +' '+ $requestAnswer['otherAnswer'];
}
}
return $answers;
}
We do not know how the $answers is going to be used. Because returning both answer and otherAnswer requires $answers to be array. Which means any user of $answers must be refactored.
Something like this might work:
$answers[$questionId] = ["answer" => $requestAnswer["answer"], "otherAnswers" => requestAnswer["otherAnswer"] ]
Just add some checking mechanism for the key existence.
Or you can concatenate it as suggested in another SO answer.
As you said you want both values then you can store into array as like below
$answers[$questionUid][] = $requestAnswer['answer'];
$answers[$questionUid][] = $requestAnswer['otherAnswer'];
By using above script $answers would have both values against $questionUid
Assuming that $answers[$questionUid] is an array, You can try to push the values to it:
/** #noinspection PhpWrongForeachArgumentTypeInspection */
foreach ($requestAnswers as $questionUid => $requestAnswer) {
$answers[$questionUid] = $requestAnswer['answer'] . '----' . $requestAnswer['otherAnswer'];
}
Hope this helps.

PhpStorm: extract() identifying variables

Does anyone know whether there is a setting in PhpStorm that can trigger identifying variables generated using extract() function?
Example would be something like the following:
/**
* #return array
*/
protected function orderSet() : array
{
//...
return [
'colour' => $colour,
'green' => $green,
'orange' => $orange
];
}
/**
* #test
*/
public function returns_correct_attribute_names()
{
$params = $this->orderSet();
extract($params);
$this->assertEquals(
'Colour',
$colour->name
);
}
At the moment any variable that's been extracted in the test is highlighted (unrecognised), but perhaps there is a setting that can change this behaviour?
The solution that LazyOne offered actually works. However there is a bit more context you need in order to implement it.
To accurately inform PHPSTORM about the variables you want to declare the comment must be placed directly above extract() and not the parent function.
public function db(){
$db = new SQLite3('db/mysqlitedb.db');
$payments = $db->query('SELECT * FROM payments');
while ($pay = $payments->fetchArray()){
/**
* #var string $to_user
* #var string $from_user
* #var number $amount
*/
extract($pay);
if (isset($to_user, $from_user, $amount))
echo "TO: {$to_user}| FROM: {$from_user}| $ {$amount} \n";
};
}
This is a working sample from my code ( couldn't copy yours for some reason ).
You can see just before I use the extract() function I declare in the comment block above it the hidden variables and data types.
Bonus: if you intend to use extract, I highly recommend you use an isset to ensure the array you're parsing contains the fields you are expecting.
example in code above

How to merge two php Doctrine 2 ArrayCollection()

Is there any convenience method that allows me to concatenate two Doctrine ArrayCollection()? something like:
$collection1 = new ArrayCollection();
$collection2 = new ArrayCollection();
$collection1->add($obj1);
$collection1->add($obj2);
$collection1->add($obj3);
$collection2->add($obj4);
$collection2->add($obj5);
$collection2->add($obj6);
$collection1->concat($collection2);
// $collection1 now contains {$obj1, $obj2, $obj3, $obj4, $obj5, $obj6 }
I just want to know if I can save me iterating over the 2nd collection and adding each element one by one to the 1st collection.
Thanks!
Better (and working) variant for me:
$collection3 = new ArrayCollection(
array_merge($collection1->toArray(), $collection2->toArray())
);
You can simply do:
$a = new ArrayCollection();
$b = new ArrayCollection();
...
$c = new ArrayCollection(array_merge((array) $a, (array) $b));
If you are required to prevent any duplicates, this snippet might help. It uses a variadic function parameter for usage with PHP5.6.
/**
* #param array... $arrayCollections
* #return ArrayCollection
*/
public function merge(...$arrayCollections)
{
$returnCollection = new ArrayCollection();
/**
* #var ArrayCollection $arrayCollection
*/
foreach ($arrayCollections as $arrayCollection) {
if ($returnCollection->count() === 0) {
$returnCollection = $arrayCollection;
} else {
$arrayCollection->map(function ($element) use (&$returnCollection) {
if (!$returnCollection->contains($element)) {
$returnCollection->add($element);
}
});
}
}
return $returnCollection;
}
Might be handy in some cases.
$newCollection = new ArrayCollection((array)$collection1->toArray() + $collection2->toArray());
This should be faster than array_merge. Duplicate key names from $collection1 are kept when same key name is present in $collection2. No matter what the actual value is
You still need to iterate over the Collections to add the contents of one array to another. Since the ArrayCollection is a wrapper class, you could try merging the arrays of elements while maintaining the keys, the array keys in $collection2 override any existing keys in $collection1 using a helper function below:
$combined = new ArrayCollection(array_merge_maintain_keys($collection1->toArray(), $collection2->toArray()));
/**
* Merge the arrays passed to the function and keep the keys intact.
* If two keys overlap then it is the last added key that takes precedence.
*
* #return Array the merged array
*/
function array_merge_maintain_keys() {
$args = func_get_args();
$result = array();
foreach ( $args as &$array ) {
foreach ( $array as $key => &$value ) {
$result[$key] = $value;
}
}
return $result;
}
Add a Collection to an array, based on Yury Pliashkou's comment (I know it does not directly answer the original question, but that was already answered, and this could help others landing here):
function addCollectionToArray( $array , $collection ) {
$temp = $collection->toArray();
if ( count( $array ) > 0 ) {
if ( count( $temp ) > 0 ) {
$result = array_merge( $array , $temp );
} else {
$result = $array;
}
} else {
if ( count( $temp ) > 0 ) {
$result = $temp;
} else {
$result = array();
}
}
return $result;
}
Maybe you like it... maybe not... I just thought of throwing it out there just in case someone needs it.
Attention! Avoid large nesting of recursive elements. array_unique - has a recursive embedding limit and causes a PHP error Fatal error: Nesting level too deep - recursive dependency?
/**
* #param ArrayCollection[] $arrayCollections
*
* #return ArrayCollection
*/
function merge(...$arrayCollections) {
$listCollections = [];
foreach ($arrayCollections as $arrayCollection) {
$listCollections = array_merge($listCollections, $arrayCollection->toArray());
}
return new ArrayCollection(array_unique($listCollections, SORT_REGULAR));
}
// using
$a = new ArrayCollection([1,2,3,4,5,6]);
$b = new ArrayCollection([7,8]);
$c = new ArrayCollection([9,10]);
$result = merge($a, $b, $c);
Combine the spread operator to merge multiple collections, e.g. all rows in all sheets of a spreadsheet, where both $sheets and $rows are ArrayCollections and have a getRows(): Collection method
// Sheet.php
public function getRows(): Collection { return $this->rows; }
// Spreadsheet.php
public function getSheets(): Collection { return $this->sheets; }
public function getRows(): Collection
return array_merge(...$this->getSheets()->map(
fn(Sheet $sheet) => $sheet->getRows()->toArray()
));
Using Clousures PHP5 > 5.3.0
$a = ArrayCollection(array(1,2,3));
$b = ArrayCollection(array(4,5,6));
$b->forAll(function($key,$value) use ($a){ $a[]=$value;return true;});
echo $a.toArray();
array (size=6) 0 => int 1 1 => int 2 2 => int 3 3 => int 4 4 => int 5 5 => int 6

Categories