How to calculate arrays of objects intersection with custom comparison function? - php

I have two arrays of objects and I need to get array of objects which are present in both arrays, comparing them with custom callback function.
Here is my code:
<?php
class Some {
public $prop1;
public $prop2;
public function __construct($prop1, $prop2)
{
$this->prop1 = $prop1;
$this->prop2 = $prop2;
}
}
$arr1 = [new Some(1, 2), new Some(2, 3), new Some(3, 4)];
$arr2 = [new Some(2, 3), new Some(1, 2)];
$intersection = array_uintersect($arr1, $arr2, function ($el1, $el2) {
return ($el1->prop1 === $el2->prop1) && ($el1->prop2 === $el2->prop2) ? 0 : 1;
});
print_r($intersection);
And what I get is:
Array (
[1] => Some Object
(
[prop1] => 2
[prop2] => 3
)
)
While I obviously want to get two objects which props are identical ((1, 2) and (2, 3).
What's wrong with this uintersect? How to achieve what I need?

Intersect function first sorted arrays, so you need have understanding haw one object less than another, and return -1, 0, 1 as result of comparing. Else, if you only can answer that objects are equal or not, you should scan second array for each item of the first one – splash58 13 mins ago. Now result of the code depends on order of objects in your arrays
To use intersect function you can create a function to compare two objects as below
class Some {
public $prop1;
public $prop2;
static function compare($el1, $el2) {
$r = $el1->prop1 - $el2->prop1;
return $r ? $r : ($el1->prop2 - $el2->prop2);
}
public function __construct($prop1, $prop2)
{
$this->prop1 = $prop1;
$this->prop2 = $prop2;
}
}
$arr1 = [new Some(1, 2), new Some(2, 3), new Some(3, 4)];
$arr2 = [new Some(2, 3), new Some(1, 2)];
$intersection = array_uintersect($arr1, $arr2, ['Some','compare']);
print_r($intersection);
demo
UPDATE
If you don't want to make function to compare objects, PHP itself checks that such objects are equal. So you can use simple code with in_array function
$intersection = array();
foreach($arr1 as $x) {
if (in_array($x, $arr2)) {
$intersection[] = $x;
}
}
print_r($intersection);

Related

Shouldn't array_udiff call my function for every iteration of my two arrays?

Running on php 7.3
Before someone asks, I have tested my compare function and it seems to work perfectly. If compare($a, $b) is 1 then compare($b, $a) is -1. If equal the inverse is still equal.The 'objects' below are all of the same class.
class myObject(){
private $a;
private $b;
private $c;
private $d;
public function __construct($a, $b, $c, $d){
$this->a = $a;
$this->b = $b;
$this->c = $c;
$this->d = $d;
}
public static function compare(myObject $object_1, myObject $object_2){
//This is somewhat complicated but my tests show that it works.
return $result; // -1, 0. or 1
}
public function hasTwoNulls(){
return $this->c === null && $this->d === null;
}
}
$object_1 = new myObject('string', 'string', null, null);
$object_A = new myObject('string', 'string', null, null);
//etc....
$array_1 =[ $object_1, $object_2, //etc...];
$array_2 =[ $object_A, $object_B, //etc...];
$diff = array_udiff(
$array_1,
$array_2,
static function( $a, $b ) {
if($a->hasTwoNulls() && $b->hasTwoNulls()){
print 'I have a breakpoint on this line that is never reached!!!.';
}
return myObject::compare( $a, $b);
}
);
As noted above, the breakpoint where I test for two nulls in each object is never reached even though it should be. I'm confused why it is never testing those objects together and why that object always ends up in my $diff even when I myObject::compare( $a, $b); those objects I get 0, aka equal.
It seems like it is doing some sort of comparisons without using my callback. Anyone have a read on this sorcery?
array_udiff() apparently has some optimizations to avoid comparing every pair of elements. I'm not sure of the exact algorithm, but I think it's sorting the two arrays and removing duplicates, and then stepping through them to find the matching elements.
You can see this with a much simpler example
$array1 = [1, 2, 3];
$array2 = [4, 5, 6];
var_dump(array_udiff($array1, $array2, function($a, $b) {
echo "Comparing $a and $b<br>";
if ($a < 4 && $b < 4) {
return 0;
} elseif ($a > 3 && $b > 3) {
return 0;
} else {
return rand(-1, 1);
}
}));
Produces:
Comparing 1 and 2
Comparing 2 and 3
Comparing 4 and 5
Comparing 5 and 6
Comparing 1 and 4
Comparing 1 and 5
Comparing 1 and 6
Comparing 1 and 2
Comparing 2 and 3
array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) }

How do I fill an object in PHP from an Array

Suppose I have:
class A{
public $one;
public $two;
}
and an array with values:
array('one' => 234, 'two' => 2)
is there a way to have an instance of A filled with the right values from the array automatically?
You need to write yourself a function for that. PHP has get_object_varsDocs but no set counterpart:
function set_object_vars($object, array $vars) {
$has = get_object_vars($object);
foreach ($has as $name => $oldValue) {
$object->$name = isset($vars[$name]) ? $vars[$name] : NULL;
}
}
Usage:
$a = new A();
$vars = array('one' => 234, 'two' => 2);
set_object_vars($a, $vars);
If you want to allow for bulk-setting of attributes, you can also store them as a property. It allows you to encapsulate within the class a little better.
class A{
protected $attributes = array();
function setAttributes($attributes){
$this->attributes = $attributes;
}
public function __get($key){
return $this->attributes[$key];
}
}
#hakre version is quite good, but dangerous (suppose an id or password is in thoses props).
I would change the default behavior to that:
function set_object_vars($object, array $vars) {
$has = get_object_vars($object);
foreach ($has as $name => $oldValue) {
array_key_exists($name, $vars) ? $object->$name =$vars[$name] : NULL;
}
}
here, the previous properties that are not in the $vars array are not affected.
and if you want to set a prop to null on purpose, you can.
Yes there is.
You could use a pass thru method.
For example:
class A {
public $one, $tow;
function __construct($values) {
$this->one = $values['one'] ?: null;
$this->two = $values['two'] ?: null;
}
}
$a = new A(array('one' => 234, 'two' => 2));

PHP: Sorting custom classes, using java-like Comparable?

How can I make my own custom class be sortable using sort() for example?
I've been scanning the web to find any method of making a class Comparable like in Java but without much luck. I tried implementing __equals() but without luck. I've also tried with __toString(). My class looks like this:
class Genre {
private $genre;
private $count;
...
}
I want to sort them by count which is an Integer, in descending order... ($genre is a string)
You can create a custom sort method and use the http://www.php.net/manual/en/function.usort.php function to call it.
Example:
$Collection = array(..); // An array of Genre objects
// Either you must make count a public variable, or create
// an accessor function to access it
function CollectionSort($a, $b)
{
if ($a->count == $b->count)
{
return 0;
}
return ($a->count < $b->count) ? -1 : 1;
}
usort($Collection, "CollectionSort");
If you'd like to make a more generic collection system you could try something like this
interface Sortable
{
public function GetSortField();
}
class Genre implements Sortable
{
private $genre;
private $count;
public function GetSortField()
{
return $count;
}
}
class Collection
{
private $Collection = array();
public function AddItem($Item)
{
$this->Collection[] = $Item;
}
public function GetItems()
{
return $this->Collection;
}
public function Sort()
{
usort($this->Collection, 'GenericCollectionSort');
}
}
function GenericCollectionSort($a, $b)
{
if ($a->GetSortField() == $b->GetSortField())
{
return 0;
}
return ($a->GetSortField() < $b->GetSortField()) ? -1 : 1;
}
$Collection = new Collection();
$Collection->AddItem(...); // Add as many Genre objects as you want
$Collection->Sort();
$SortedGenreArray = $Collection->GetItems();
maybe you can use the function "usort":
class Genre {
private $genre;
private $count;
...
public function __construct($g, $c)
{
$this->genre=g;
$this->count=c;
}
public static function compare($a, $b)
{
if ($a->count < $b->count) return -1;
else if($a->count == $b->count) return 0;
else return 1;
}
...
}
$genres= array(
new Genre (1, 5),
new Genre (2, 2),
new Genre (3, 7)
);
usort($genres, array("Genre", "compare"));
Regards Thomas
The simplest way to do this is to simply use usort on a method within the class that accepts as input an array of matching objects. This is example 3 in the documentation linked to above. However, this is somewhat clunky and ugly.
A better way is to create a new type of array class specific to the desired objects using ArrayAccess. This will enable you to use the custom array like you'd expect but then also be able to run arbitrary sorting methods on the array.
Under the hood, you'd likely still want to use usort, but you'd be hiding that fact behind a much nicer interface that might look like this:
$array = new GenreCollection();
$array[] = new Genre(1); // Genre's constructor is a factory that can load genres via an ID
$array[] = new Genre(2);
$array[] = new Genre(3);
$array[] = new Genre(4);
$array[] = new Genre(5);
// Example Sorts
$array->SortByName();
$array->SortByDateInvented();
$array->SortByID();
$array->SortBySubGenres(); // Arranges Genres into a hierarchy where 'Death Metal' comes with other metal after 'Heavy Metal' - Add in a nest-level value for making dropdowns and other nested lists.
__toString() works for me:
class Genre
{
private $genre;
private $count;
public function __construct( $genre, $count = 0 )
{
$this->genre = $genre;
$this->count = $count;
}
public function __toString()
{
return $this->count . ' ' . $this->genre;
}
}
$collection = array(
new Genre( 'alternative', 3 ),
new Genre( 'jazz', 2 ),
new Genre( 'hiphop', 1 ),
new Genre( 'heavy metal', 1 )
);
natsort( $collection );
foreach( $collection as $genre )
{
echo $genre . "\n";
}
Produces:
1 heavy metal
1 hiphop
2 jazz
3 alternative

Removing duplicate objects from arrays? [duplicate]

Is there any method like the array_unique for objects? I have a bunch of arrays with 'Role' objects that I merge, and then I want to take out the duplicates :)
array_unique works with an array of objects using SORT_REGULAR:
class MyClass {
public $prop;
}
$foo = new MyClass();
$foo->prop = 'test1';
$bar = $foo;
$bam = new MyClass();
$bam->prop = 'test2';
$test = array($foo, $bar, $bam);
print_r(array_unique($test, SORT_REGULAR));
Will print:
Array (
[0] => MyClass Object
(
[prop] => test1
)
[2] => MyClass Object
(
[prop] => test2
)
)
See it in action here: http://3v4l.org/VvonH#v529
Warning: it will use the "==" comparison, not the strict comparison ("===").
So if you want to remove duplicates inside an array of objects, beware that it will compare each object properties, not compare object identity (instance).
Well, array_unique() compares the string value of the elements:
Note: Two elements are considered equal if and only if (string) $elem1 === (string) $elem2 i.e. when the string representation is the same, the first element will be used.
So make sure to implement the __toString() method in your class and that it outputs the same value for equal roles, e.g.
class Role {
private $name;
//.....
public function __toString() {
return $this->name;
}
}
This would consider two roles as equal if they have the same name.
This answer uses in_array() since the nature of comparing objects in PHP 5 allows us to do so. Making use of this object comparison behaviour requires that the array only contain objects, but that appears to be the case here.
$merged = array_merge($arr, $arr2);
$final = array();
foreach ($merged as $current) {
if ( ! in_array($current, $final)) {
$final[] = $current;
}
}
var_dump($final);
Here is a way to remove duplicated objects in an array:
<?php
// Here is the array that you want to clean of duplicate elements.
$array = getLotsOfObjects();
// Create a temporary array that will not contain any duplicate elements
$new = array();
// Loop through all elements. serialize() is a string that will contain all properties
// of the object and thus two objects with the same contents will have the same
// serialized string. When a new element is added to the $new array that has the same
// serialized value as the current one, then the old value will be overridden.
foreach($array as $value) {
$new[serialize($value)] = $value;
}
// Now $array contains all objects just once with their serialized version as string.
// We don't care about the serialized version and just extract the values.
$array = array_values($new);
You can also serialize first:
$unique = array_map( 'unserialize', array_unique( array_map( 'serialize', $array ) ) );
As of PHP 5.2.9 you can just use optional sort_flag SORT_REGULAR:
$unique = array_unique( $array, SORT_REGULAR );
You can also use they array_filter function, if you want to filter objects based on a specific attribute:
//filter duplicate objects
$collection = array_filter($collection, function($obj)
{
static $idList = array();
if(in_array($obj->getId(),$idList)) {
return false;
}
$idList []= $obj->getId();
return true;
});
From here: http://php.net/manual/en/function.array-unique.php#75307
This one would work with objects and arrays also.
<?php
function my_array_unique($array, $keep_key_assoc = false)
{
$duplicate_keys = array();
$tmp = array();
foreach ($array as $key=>$val)
{
// convert objects to arrays, in_array() does not support objects
if (is_object($val))
$val = (array)$val;
if (!in_array($val, $tmp))
$tmp[] = $val;
else
$duplicate_keys[] = $key;
}
foreach ($duplicate_keys as $key)
unset($array[$key]);
return $keep_key_assoc ? $array : array_values($array);
}
?>
If you have an indexed array of objects, and you want to remove duplicates by comparing a specific property in each object, a function like the remove_duplicate_models() one below can be used.
class Car {
private $model;
public function __construct( $model ) {
$this->model = $model;
}
public function get_model() {
return $this->model;
}
}
$cars = [
new Car('Mustang'),
new Car('F-150'),
new Car('Mustang'),
new Car('Taurus'),
];
function remove_duplicate_models( $cars ) {
$models = array_map( function( $car ) {
return $car->get_model();
}, $cars );
$unique_models = array_unique( $models );
return array_values( array_intersect_key( $cars, $unique_models ) );
}
print_r( remove_duplicate_models( $cars ) );
The result is:
Array
(
[0] => Car Object
(
[model:Car:private] => Mustang
)
[1] => Car Object
(
[model:Car:private] => F-150
)
[2] => Car Object
(
[model:Car:private] => Taurus
)
)
sane and fast way if you need to filter duplicated instances (i.e. "===" comparison) out of array and:
you are sure what array holds only objects
you dont need keys preserved
is:
//sample data
$o1 = new stdClass;
$o2 = new stdClass;
$arr = [$o1,$o1,$o2];
//algorithm
$unique = [];
foreach($arr as $o){
$unique[spl_object_hash($o)]=$o;
}
$unique = array_values($unique);//optional - use if you want integer keys on output
This is very simple solution:
$ids = array();
foreach ($relate->posts as $key => $value) {
if (!empty($ids[$value->ID])) { unset($relate->posts[$key]); }
else{ $ids[$value->ID] = 1; }
}
You can also make the array unique using a callback function (e.g. if you want to compare a property of the object or whatever method).
This is the generic function I use for this purpose:
/**
* Remove duplicate elements from an array by comparison callback.
*
* #param array $array : An array to eliminate duplicates by callback
* #param callable $callback : Callback accepting an array element returning the value to compare.
* #param bool $preserveKeys : Add true if the keys should be perserved (note that if duplicates eliminated the first key is used).
* #return array: An array unique by the given callback
*/
function unique(array $array, callable $callback, bool $preserveKeys = false): array
{
$unique = array_intersect_key($array, array_unique(array_map($callback, $array)));
return ($preserveKeys) ? $unique : array_values($unique);
}
Sample usage:
$myUniqueArray = unique($arrayToFilter,
static function (ExamQuestion $examQuestion) {
return $examQuestion->getId();
}
);
array_unique version for strict (===) comparison, preserving keys:
function array_unique_strict(array $array): array {
$result = [];
foreach ($array as $key => $item) {
if (!in_array($item, $result, true)) {
$result[$key] = $item;
}
}
return $result;
}
Usage:
class Foo {}
$foo1 = new Foo();
$foo2 = new Foo();
array_unique_strict( ['a' => $foo1, 'b' => $foo1, 'c' => $foo2] ); // ['a' => $foo1, 'c' => $foo2]
array_unique works by casting the elements to a string and doing a comparison. Unless your objects uniquely cast to strings, then they won't work with array_unique.
Instead, implement a stateful comparison function for your objects and use array_filter to throw out things the function has already seen.
This is my way of comparing objects with simple properties, and at the same time receiving a unique collection:
class Role {
private $name;
public function __construct($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
}
$roles = [
new Role('foo'),
new Role('bar'),
new Role('foo'),
new Role('bar'),
new Role('foo'),
new Role('bar'),
];
$roles = array_map(function (Role $role) {
return ['key' => $role->getName(), 'val' => $role];
}, $roles);
$roles = array_column($roles, 'val', 'key');
var_dump($roles);
Will output:
array (size=2)
'foo' =>
object(Role)[1165]
private 'name' => string 'foo' (length=3)
'bar' =>
object(Role)[1166]
private 'name' => string 'bar' (length=3)
If you have array of objects and you want to filter this collection to remove all duplicates you can use array_filter with anonymous function:
$myArrayOfObjects = $myCustomService->getArrayOfObjects();
// This is temporary array
$tmp = [];
$arrayWithoutDuplicates = array_filter($myArrayOfObjects, function ($object) use (&$tmp) {
if (!in_array($object->getUniqueValue(), $tmp)) {
$tmp[] = $object->getUniqueValue();
return true;
}
return false;
});
Important: Remember that you must pass $tmp array as reference to you filter callback function otherwise it will not work

PHP : Remove object from array

What is an elegant way to remove an object from an array of objects in PHP?
class Data{
private $arrObservers;
public add(Observer $o) {
array_push($this->arrObservers, $o);
}
public remove(Observer $o) {
// I NEED THIS CODE to remove $o from $this->arrObservers
}
}
You can do
function unsetValue(array $array, $value, $strict = TRUE)
{
if(($key = array_search($value, $array, $strict)) !== FALSE) {
unset($array[$key]);
}
return $array;
}
You can also use spl_object_hash to create a hash for the objects and use that as array key.
However, PHP also has a native Data Structure for Object collections with SplObjectStorage:
$a = new StdClass; $a->id = 1;
$b = new StdClass; $b->id = 2;
$c = new StdClass; $c->id = 3;
$storage = new SplObjectStorage;
$storage->attach($a);
$storage->attach($b);
$storage->attach($c);
echo $storage->count(); // 3
// trying to attach same object again
$storage->attach($c);
echo $storage->count(); // still 3
var_dump( $storage->contains($b) ); // TRUE
$storage->detach($b);
var_dump( $storage->contains($b) ); // FALSE
SplObjectStorage is Traversable, so you can foreach over it as well.
On a sidenote, PHP also has native interfaces for Subject and Observer.
I agree with the answers above, but for the sake of completeness (where you may not have unique IDs to use as a key) my preferred methods of removing values from an array are as follows:
/**
* Remove each instance of a value within an array
* #param array $array
* #param mixed $value
* #return array
*/
function array_remove(&$array, $value)
{
return array_filter($array, function($a) use($value) {
return $a !== $value;
});
}
/**
* Remove each instance of an object within an array (matched on a given property, $prop)
* #param array $array
* #param mixed $value
* #param string $prop
* #return array
*/
function array_remove_object(&$array, $value, $prop)
{
return array_filter($array, function($a) use($value, $prop) {
return $a->$prop !== $value;
});
}
Which are used in the following way:
$values = array(
1, 2, 5, 3, 5, 6, 7, 1, 2, 4, 5, 6, 6, 8, 8,
);
print_r(array_remove($values, 6));
class Obj {
public $id;
public function __construct($id) {
$this->id = $id;
}
}
$objects = array(
new Obj(1), new Obj(2), new Obj(4), new Obj(3), new Obj(6), new Obj(4), new Obj(3), new Obj(1), new Obj(5),
);
print_r(array_remove_object($objects, 1, 'id'));
I recommend using the ID (if you have one, anything that will be unique to that object should work within reason) of the object as the array key. This way you can address the object within the array without having to run through a loop or store the ID in another location. The code would look something like this:
$obj_array[$obj1->getId()] = $obj1;
$obj_array[$obj2->getId()] = $obj2;
$obj_array[$obj3->getId()] = $obj3;
unset($obj_array[$object_id]);
UPDATE:
class Data{
private $arrObservers;
public add(Observer $o) {
$this->arrObservers[$o->getId()] = $o;
}
public remove(Observer $o) {
unset($this->arrObservers[$o->getId()]);
}
}
unset($myArray[$index]); where $index is the index of the element you want to remove. If you wan't a more specific answer, show some code or describe what you're trying to do.
$obj_array['obj1'] = $obj1;
$obj_array['obj2'] = $obj2;
$obj_array['obj3'] = $obj3;
unset($obj_array['obj3']);
For remove an object from a multi dimensional array you can use this:
$exampleArray= [
[
"myKey"=>"This is my key",
"myValue"=>"10"
],
[
"myKey"=>"Oh!",
"myValue"=>"11"
]
];
With array_column you can specify your key column name:
if(($key = array_search("Oh!", array_column($exampleArray, 'myKey'))) !== false) {
unset($exampleArray[$key]);
}
And this will remove the indicated object.
Use this for your internal object storage instead: http://us2.php.net/manual/en/class.splobjectstorage.php
function obj_array_clean ($array, $objId)
{
$new = array() ;
foreach($array as $value)
{
$new[$value->{$objId}] = $value;
}
$array = array_values($new);
return $array;
}
$ext2 = obj_array_clean($ext, 'OnjId');
It will remove the duplicate object "OnjId" from array objects $array.
If you want to remove one or more objects from array of objects (using spl_object_hash to determine if objects are the same) you can use this method:
$this->arrObservers = Arr::diffObjects($this->arrObservers, [$o]);
from this library.
Reading the Observer pattern part of the GoF book? Here's a solution that will eliminate the need to do expensive searching to find the index of the object that you want to remove.
public function addObserver(string $aspect, string $viewIndex, Observer $view)
{
$this->observers[$aspect][$viewIndex] = $view;
}
public function removeObserver(string $aspect, string $viewIndex)
{
if (!isset($this->observers[$aspect])) {
throw new OutOfBoundsException("No such aspect ({$aspect}) of this Model exists: " . __CLASS__);
}
if (!isset($this->observers[$aspect][$viewIndex])) {
throw new OutOfBoundsException("No such View for ({$viewIndex}) was added to the aspect ({$aspect}) of this Model:" . __CLASS__);
}
unset($this->observers[$aspect][$viewIndex]);
}
You can loose the "aspect" dimension if you are not using that way of keeping track of which Views are updated by specific Models.
public function addObserver(string $viewIndex, Observer $view)
{
$this->observers[$viewIndex] = $view;
}
public function removeObserver(string $viewIndex)
{
if (!isset($this->observers[$viewIndex])) {
throw new OutOfBoundsException("No such View for ({$viewIndex}) was added to this Model:" . __CLASS__);
}
unset($this->observers[$viewIndex]);
}
Summary
Build in a way to find the element before assigning the object to the array. Otherwise, you will have to discover the index of the object element first.
If you have a large number of object elements (or, even more than a handful), then you may need to resort to finding the index of the object first. The PHP function array_search() is one way to start with a value, and get the index/key in return.
https://www.php.net/manual/en/function.array-search.php
Do be sure to use the strict argument when you call the function.
If the third parameter strict is set to true then the array_search()
function will search for identical elements in the haystack. This
means it will also perform a strict type comparison of the needle in
the haystack, and objects must be the same instance.
Try this, will solve your problem.
class Data{
private $arrObservers;
public add(Observer $o) {
array_push($this->arrObservers,$o);
}
public remove($Observer $o) {
unset($this->arrObservers[$o]);
}
}
I believe this is the best way
$index = array_search($o, $this->arrObservers, true);
unset($this->arrObservers[$index]);

Categories