¿Cuál es el mejor método para fusionar dos objetos PHP?

222

Tenemos dos objetos PHP5 y nos gustaría fusionar el contenido de uno en el segundo. No existe una noción de subclases entre ellos, por lo que las soluciones descritas en el siguiente tema no pueden aplicarse.

¿Cómo se copia un objeto PHP en un tipo de objeto diferente?

//We have this:
$objectA->a;
$objectA->b;
$objectB->c;
$objectB->d;

//We want the easiest way to get:
$objectC->a;
$objectC->b;
$objectC->c;
$objectC->d;

Observaciones:

  • Estos son objetos, no clases.
  • Los objetos contienen muchos campos, por lo que un foreach sería bastante lento.
  • Hasta ahora consideramos transformar los objetos A y B en matrices y luego fusionarlos usando array_merge () antes de volver a transformarlos en un objeto, pero no podemos decir que estamos orgullosos de esto.
Veynom
fuente
30
"Los objetos contienen muchos campos, por lo que un foreach sería bastante lento". - Las computadoras son bastante rápidas, "bastante lento" suele ser lo suficientemente rápido.
Sean McSomething

Respuestas:

435

Si sus objetos solo contienen campos (sin métodos), esto funciona:

$obj_merged = (object) array_merge((array) $obj1, (array) $obj2);

En realidad, esto también funciona cuando los objetos tienen métodos. (probado con PHP 5.3 y 5.6)

flochtililoch
fuente
1
También puede usar array_merge_recursive para tener un comportamiento de copia profunda. También te puede interesar array_replace_recursive. Las diferencias se explican en detalle aquí: brian.serveblog.net/2011/07/31/php-array_replace-vs-array_merge
Vincent Pazeller
12
El objeto resultante de esto será una instancia de stdclass. Si bien en cierto sentido "funciona" en objetos con métodos, efectivamente arruina el objeto en ese caso (al eliminar los métodos).
Brilliand
Esto es útil para devolver múltiples conjuntos de resultados en una sola función (y devolver solo un objeto con pares clave-valor).
Leonel Atencio
1
Esto no funcionará si hay una clave entera en el objeto. Considere el siguiente ejemplo: $ arr1 = array ('a' => 9, 'b' => 'asd'); $ arr2 = array ('a' => 10, 'd' => 'qwert', 0 => 100, 1 => 200, 4 => 400); $ arr3 = array_merge ($ arr1, $ arr2); echo (print_r ($ arr3, 1)); Salida real: matriz ([a] => 10 [b] => asd [d] => qwert [0] => 100 [1] => 200 [2] => 400) Salida deseada: matriz ([a] => 10 [b] => asd [d] => qwert [0] => 100 [1] => 200 [4] => 400)
Souvik
2
¿Soy solo yo o esta respuesta es una copia literal de una respuesta que ya se había publicado durante meses? stackoverflow.com/a/794356/151509
maryisdead
28

Podría crear otro objeto que despache llamadas a métodos mágicos a los objetos subyacentes. Así es como lo manejarías __get, pero para que funcione completamente, tendrás que anular todos los métodos mágicos relevantes. Probablemente encontrará errores de sintaxis ya que lo acabo de ingresar en la parte superior de mi cabeza.

class Compositor {
  private $obj_a;
  private $obj_b;

  public function __construct($obj_a, $obj_b) {
    $this->obj_a = $obj_a;
    $this->obj_b = $obj_b;
  }

  public function __get($attrib_name) {
    if ($this->obj_a->$attrib_name) {
       return $this->obj_a->$attrib_name;
    } else {
       return $this->obj_b->$attrib_name;
    }
  }
}

Buena suerte.

Allain Lalonde
fuente
La implementación completa probablemente necesitará __isset (), __unset () e implementará la interfaz Interator.
Kornel
@porneL: ¿qué es la interfaz Interator?
Pim Jager
2
Editaría su comentario, pero no puedes hacer eso. Creo que se refiere a Iterator
Allain Lalonde
Me gusta mucho tu solución, Allain, pero me temo que significa que tenemos que reescribir toda nuestra aplicación si decidimos usarla.
Veynom el
3
Ok ... entonces elige el camino que no requiere una reescritura completa.
Allain Lalonde
25
foreach($objectA as $k => $v) $objectB->$k = $v;
Kornel
fuente
66
Esto es más rápido que la respuesta aceptada en las versiones de PHP <7 (estimado 50% más rápido). Pero en PHP> = 7, la respuesta aceptada es algo así como un 400% más rápido. Ver aquí: sandbox.onlinephpfunctions.com/code/…
yunzen
¿Cómo podemos usar u obtener los datos combinados aquí?
1
@ramedju En este ejemplo $objectBcontiene los datos combinados.
Kornel
10

Entiendo que usar los objetos genéricos [stdClass ()] y convertirlos como matrices responde la pregunta, pero pensé que el Compositor fue una gran respuesta. Sin embargo, sentí que podría usar algunas mejoras de funciones y podría ser útil para otra persona.

caracteristicas:

  • Especificar referencia o clon
  • Especifique la primera o la última entrada para tener prioridad
  • Fusión de objetos múltiples (más de dos) con similitud de sintaxis a array_merge
  • Enlace de método: $ obj-> f1 () -> f2 () -> f3 () ...
  • Compuestos dinámicos : $ obj-> merge (...) / * trabajar aquí * / $ obj-> merge (...)

Código:

class Compositor {

    protected $composite = array();
    protected $use_reference;
    protected $first_precedence;

    /**
     * __construct, Constructor
     *
     * Used to set options.
     *
     * @param bool $use_reference whether to use a reference (TRUE) or to copy the object (FALSE) [default]
     * @param bool $first_precedence whether the first entry takes precedence (TRUE) or last entry takes precedence (FALSE) [default]
     */
    public function __construct($use_reference = FALSE, $first_precedence = FALSE) {
        // Use a reference
        $this->use_reference = $use_reference === TRUE ? TRUE : FALSE;
        $this->first_precedence = $first_precedence === TRUE ? TRUE : FALSE;

    }

    /**
     * Merge, used to merge multiple objects stored in an array
     *
     * This is used to *start* the merge or to merge an array of objects.
     * It is not needed to start the merge, but visually is nice.
     *
     * @param object[]|object $objects array of objects to merge or a single object
     * @return object the instance to enable linking
     */

    public function & merge() {
        $objects = func_get_args();
        // Each object
        foreach($objects as &$object) $this->with($object);
        // Garbage collection
        unset($object);

        // Return $this instance
        return $this;
    }

    /**
     * With, used to merge a singluar object
     *
     * Used to add an object to the composition
     *
     * @param object $object an object to merge
     * @return object the instance to enable linking
     */
    public function & with(&$object) {
        // An object
        if(is_object($object)) {
            // Reference
            if($this->use_reference) {
                if($this->first_precedence) array_push($this->composite, $object);
                else array_unshift($this->composite, $object);
            }
            // Clone
            else {
                if($this->first_precedence) array_push($this->composite, clone $object);
                else array_unshift($this->composite, clone $object);
            }
        }

        // Return $this instance
        return $this;
    }

    /**
     * __get, retrieves the psudo merged object
     *
     * @param string $name name of the variable in the object
     * @return mixed returns a reference to the requested variable
     *
     */
    public function & __get($name) {
        $return = NULL;
        foreach($this->composite as &$object) {
            if(isset($object->$name)) {
                $return =& $object->$name;
                break;
            }
        }
        // Garbage collection
        unset($object);

        return $return;
    }
}

Uso:

$obj = new Compositor(use_reference, first_precedence);
$obj->merge([object $object [, object $object [, object $...]]]);
$obj->with([object $object]);

Ejemplo:

$obj1 = new stdClass();
$obj1->a = 'obj1:a';
$obj1->b = 'obj1:b';
$obj1->c = 'obj1:c';

$obj2 = new stdClass();
$obj2->a = 'obj2:a';
$obj2->b = 'obj2:b';
$obj2->d = 'obj2:d';

$obj3 = new Compositor();
$obj3->merge($obj1, $obj2);
$obj1->c = '#obj1:c';
var_dump($obj3->a, $obj3->b, $obj3->c, $obj3->d);
// obj2:a, obj2:b, obj1:c, obj2:d
$obj1->c;

$obj3 = new Compositor(TRUE);
$obj3->merge($obj1)->with($obj2);
$obj1->c = '#obj1:c';
var_dump($obj3->a, $obj3->b, $obj3->c, $obj3->d);
// obj1:a, obj1:b, obj1:c, obj2:d
$obj1->c = 'obj1:c';

$obj3 = new Compositor(FALSE, TRUE);
$obj3->with($obj1)->with($obj2);
$obj1->c = '#obj1:c';
var_dump($obj3->a, $obj3->b, $obj3->c, $obj3->d);
// obj1:a, obj1:b, #obj1:c, obj2:d
$obj1->c = 'obj1:c';
Ryan Schumacher
fuente
2
Solo para señalar: la referencia de tiempo de llamada se marcó como obsoleta en PHP 5.3.0 y se eliminó en PHP 5.4.0 (lo que resultó en un error fatal elevado). Para corregir el problema: Reemplazar foreach($objects as &$object) $this->with(&$object);con foreach($objects as &$object) $this->with($object);corrige el problema. Fuente: [ php.net/manual/en/language.references.pass.php]
wes.hysell
2
Además: if($this->first_precedence) array_push($this->composite, &$object); else array_unshift($this->composite, &$object);debe reemplazarse conif($this->first_precedence) array_push($this->composite, $object); else array_unshift($this->composite, $object);
wes.hysell
1
así que para resumir sus comentarios, elimine Ampersand (&) de $ object dentro: foreach (primer comentario) ... array_push, array_unshift (segundo comentario)
Chris
1
@ Chris Actualicé el código para solucionar los problemas según los comentarios anteriores.
Ryan Schumacher
En su código de 'Uso', escribió mal Compositor como Compositer
Xesau
7

Una solución muy simple teniendo en cuenta que tiene el objeto A y B:

foreach($objB AS $var=>$value){
    $objA->$var = $value;
}

Eso es todo. Ahora tiene objA con todos los valores de objB.

Jônatas Eridani
fuente
¿Por qué no lo harías simplemente: $ objB = $ objA;
Scottymeuk
2

La \ArrayObjectclase tiene la posibilidad de intercambiar la matriz actual para desconectar la referencia original . Para hacerlo, viene con dos métodos útiles: exchangeArray()y getArrayCopy(). El resto es simple array_merge()del objeto proporcionado con las ArrayObjectpropiedades públicas de s:

class MergeBase extends ArrayObject
{
     public final function merge( Array $toMerge )
     {
          $this->exchangeArray( array_merge( $this->getArrayCopy(), $toMerge ) );
     }
 }

El uso es tan fácil como esto:

 $base = new MergeBase();

 $base[] = 1;
 $base[] = 2;

 $toMerge = [ 3,4,5, ];

 $base->merge( $toMerge );
Corelmax
fuente
Esta en realidad debería ser la respuesta aceptada . Lo único bueno sería si merge($array)realmente solicitara \ArrayObjecttambién.
kaiser
2

una solución Para preservar, tanto los métodos como las propiedades de los proyectos combinados es crear una clase de combinador que pueda

  • tomar cualquier cantidad de objetos en __construct
  • acceder a cualquier método usando __call
  • acceder a cualquier propiedad usando __get

class combinator{
function __construct(){       
    $this->melt =  array_reverse(func_get_args());
      // array_reverse is to replicate natural overide
}
public function __call($method,$args){
    forEach($this->melt as $o){
        if(method_exists($o, $method)){
            return call_user_func_array([$o,$method], $args);
            //return $o->$method($args);
            }
        }
    }
public function __get($prop){
        foreach($this->melt as $o){
          if(isset($o->$prop))return $o->$prop;
        }
        return 'undefined';
    } 
}

uso simple

class c1{
    public $pc1='pc1';
    function mc1($a,$b){echo __METHOD__." ".($a+$b);}
}
class c2{
    public $pc2='pc2';
    function mc2(){echo __CLASS__." ".__METHOD__;}
}

$comb=new combinator(new c1, new c2);

$comb->mc1(1,2);
$comb->non_existing_method();  //  silent
echo $comb->pc2;
bortunac
fuente
Eso es muy inteligente, me quito el sombrero ante eso. Sin embargo, no creo que me sienta cómodo con los métodos que no se definen en la clase de objetos resultante.
Slytherin
gracias? .. por el sombrero ... Fue solo por diversión y estoy de acuerdo con usted acerca de la comodidad en el uso principalmente con respecto al autocompletado en netbeans u otro editor
bortunac
1

Me gustaría vincular el segundo objeto a una propiedad del primer objeto. Si el segundo objeto es el resultado de una función o método, use referencias. Ex:

//Not the result of a method
$obj1->extra = new Class2();

//The result of a method, for instance a factory class
$obj1->extra =& Factory::getInstance('Class2');
Adrian
fuente
1

Para fusionar cualquier cantidad de objetos sin procesar

function merge_obj(){
    foreach(func_get_args() as $a){
        $objects[]=(array)$a;
    }
    return (object)call_user_func_array('array_merge', $objects);
}
bortunac
fuente
0

Aquí hay una función que aplanará un objeto o matriz. Use esto solo si está seguro de que sus claves son únicas. Si tiene claves con el mismo nombre, se sobrescribirán. Deberá colocar esto en una clase y reemplazar "Funciones" con el nombre de su clase. Disfrutar...

function flatten($array, $preserve_keys=1, &$out = array(), $isobject=0) {
        # Flatten a multidimensional array to one dimension, optionally preserving keys.
        #
        # $array - the array to flatten
        # $preserve_keys - 0 (default) to not preserve keys, 1 to preserve string keys only, 2 to preserve all keys
        # $out - internal use argument for recursion
        # $isobject - is internally set in order to remember if we're using an object or array
        if(is_array($array) || $isobject==1)
        foreach($array as $key => $child)
            if(is_array($child))
                $out = Functions::flatten($child, $preserve_keys, $out, 1); // replace "Functions" with the name of your class
            elseif($preserve_keys + is_string($key) > 1)
                $out[$key] = $child;
            else
                $out[] = $child;

        if(is_object($array) || $isobject==2)
        if(!is_object($out)){$out = new stdClass();}
        foreach($array as $key => $child)
            if(is_object($child))
                $out = Functions::flatten($child, $preserve_keys, $out, 2); // replace "Functions" with the name of your class
            elseif($preserve_keys + is_string($key) > 1)
                $out->$key = $child;
            else
                $out = $child;

        return $out;
}

fuente
0

¡Hagámoslo simple!

function copy_properties($from, $to, $fields = null) {
    // copies properties/elements (overwrites duplicates)
    // can take arrays or objects 
    // if fields is set (an array), will only copy keys listed in that array
    // returns $to with the added/replaced properties/keys
    $from_array = is_array($from) ? $from : get_object_vars($from);
    foreach($from_array as $key => $val) {
        if(!is_array($fields) or in_array($key, $fields)) {
            if(is_object($to)) {
                $to->$key = $val;
            } else {
                $to[$key] = $val;
            }
        }
    }
    return($to);
}

Si eso no responde a su pregunta, seguramente ayudará con la respuesta. El crédito por el código anterior va para mí :)

Rolf
fuente
0

Este fragmento de código convertirá recursivamente esos datos a un solo tipo (matriz u objeto) sin los bucles foreach anidados. Espero que ayude a alguien!

Una vez que un Objeto está en formato de matriz, puede usar array_merge y convertirlo nuevamente a Objeto si es necesario.

abstract class Util {
    public static function object_to_array($d) {
        if (is_object($d))
            $d = get_object_vars($d);

        return is_array($d) ? array_map(__METHOD__, $d) : $d;
    }

    public static function array_to_object($d) {
        return is_array($d) ? (object) array_map(__METHOD__, $d) : $d;
    }
}

Forma procesal

function object_to_array($d) {
    if (is_object($d))
        $d = get_object_vars($d);

    return is_array($d) ? array_map(__FUNCTION__, $d) : $d;
}

function array_to_object($d) {
    return is_array($d) ? (object) array_map(__FUNCTION__, $d) : $d;
}

Todo el crédito va a: Jason Oakley

Drmzindec
fuente