Convertir / lanzar un objeto stdClass a otra clase

89

Estoy usando un sistema de almacenamiento de terceros que solo me devuelve objetos stdClass sin importar lo que ingrese por alguna extraña razón. Así que tengo curiosidad por saber si hay una manera de convertir un objeto stdClass en un objeto completo de un tipo determinado.

Por ejemplo, algo como:

//$stdClass is an stdClass instance
$converted = (BusinessClass) $stdClass;

Solo estoy lanzando stdClass en una matriz y la envío al constructor BusinessClass, pero tal vez haya una forma de restaurar la clase inicial que no conozco.

Nota: No estoy interesado en el tipo de respuestas 'Cambie su sistema de almacenamiento' ya que no es el punto de interés. Considérelo más una cuestión académica sobre las capacidades lingüísticas.

Salud

El poderoso pato de goma
fuente
Se explica en mi publicación después de la muestra de pseudocódigo. Estoy enviando contenido a una matriz y alimentando a un constructor automatizado.
The Mighty Rubber Duck
La respuesta de @Adam Puza es mucho mejor que el truco que se muestra en la respuesta aceptada. aunque estoy seguro de que un mapeador aún sería el método preferido
Chris
Bueno, ¿cómo PDOStatement::fetchObjectlogra esta tarea?
William Entriken

Respuestas:

88

Consulta el manual de Malabarismo de tipos sobre posibles lanzamientos.

Los yesos permitidos son:

  • (int), (integer) - conversión a entero
  • (bool), (booleano) - convertir a booleano
  • (flotar), (doble), (real) - lanzar para flotar
  • (cadena) - convertir a cadena
  • (matriz) - convertir a matriz
  • (objeto) - convertir a objeto
  • (no establecido) - convertir a NULL (PHP 5)

Tendría que escribir un asignador que realice la conversión de stdClass a otra clase concreta. No debería ser demasiado difícil de hacer.

O, si estás de humor hack, puedes adaptar el siguiente código:

function arrayToObject(array $array, $className) {
    return unserialize(sprintf(
        'O:%d:"%s"%s',
        strlen($className),
        $className,
        strstr(serialize($array), ':')
    ));
}

que pseudocasts una matriz a un objeto de cierta clase. Esto funciona primero serializando la matriz y luego cambiando los datos serializados para que representen una determinada clase. Entonces, el resultado no se serializa en una instancia de esta clase. Pero como dije, es hackish, así que espere efectos secundarios.

Para objeto a objeto, el código sería

function objectToObject($instance, $className) {
    return unserialize(sprintf(
        'O:%d:"%s"%s',
        strlen($className),
        $className,
        strstr(strstr(serialize($instance), '"'), ':')
    ));
}
Gordon
fuente
9
Este truco es una técnica inteligente. No lo usaré ya que mi forma actual de resolver el problema es más estable, pero interesante de todos modos.
The Mighty Rubber Duck
1
Terminas con un __PHP_Incomplete_Classobjeto usando este método (al menos a partir de PHP 5.6).
TIMESPLiNTER
1
@TiMESPLiNTER no, no es así. Consulte codepad.org/spGkyLzL . Asegúrese de que la clase a la que enviar se haya incluido antes de llamar a la función.
Gordon
@TiMESPLiNTER no estoy seguro de lo que quieres decir. funcionó. es un ejemplo \ Foo ahora.
Gordon
Sí, el problema es que agrega la propiedad stdClass. Entonces tienes dos fooBars (el privado de example\Fooy el público de stdClass). En cambio, reemplaza el valor.
TIMESPLiNTER
53

Puede usar la función anterior para convertir objetos de clase no similares (PHP> = 5.3)

/**
 * Class casting
 *
 * @param string|object $destination
 * @param object $sourceObject
 * @return object
 */
function cast($destination, $sourceObject)
{
    if (is_string($destination)) {
        $destination = new $destination();
    }
    $sourceReflection = new ReflectionObject($sourceObject);
    $destinationReflection = new ReflectionObject($destination);
    $sourceProperties = $sourceReflection->getProperties();
    foreach ($sourceProperties as $sourceProperty) {
        $sourceProperty->setAccessible(true);
        $name = $sourceProperty->getName();
        $value = $sourceProperty->getValue($sourceObject);
        if ($destinationReflection->hasProperty($name)) {
            $propDest = $destinationReflection->getProperty($name);
            $propDest->setAccessible(true);
            $propDest->setValue($destination,$value);
        } else {
            $destination->$name = $value;
        }
    }
    return $destination;
}

EJEMPLO:

class A 
{
  private $_x;   
}

class B 
{
  public $_x;   
}

$a = new A();
$b = new B();

$x = cast('A',$b);
$x = cast('B',$a);
Adam Puza
fuente
5
¡Esa es una solución bastante elegante , debo decir! Me pregunto qué tan bien escala ... La reflexión me asusta.
Theodore R. Smith
Hola Adam, esta solución me resolvió un problema similar aquí: stackoverflow.com/questions/35350585/… Si quieres obtener una respuesta fácil, dirígete y la marcaré. ¡Gracias!
oucil
No funciona para propiedades heredadas de clases principales.
Toilal
Acabo de usar esto como base de mi solución para sembrar mi base de datos con ID conocidos para pruebas funcionales de API con Behat. Mi problema fue que mis ID normales son UUID generados y no quería agregar un método setId () en mi entidad solo por el bien de mi capa de prueba, y no quería cargar archivos de accesorios y ralentizar las pruebas. Ahora puedo incluir @Given the user :username has the id :iden mi función y manejarlo con reflejos en la clase de contexto
nealio82
2
Gran solución, quiero agregar que $destination = new $destination();se puede intercambiar $destination = ( new ReflectionClass( $destination ) )->newInstanceWithoutConstructor();si necesita evitar llamar al constructor.
Scuzzy
14

Para mover todas las propiedades existentes de a stdClassa un nuevo objeto de un nombre de clase especificado:

/**
 * recast stdClass object to an object with type
 *
 * @param string $className
 * @param stdClass $object
 * @throws InvalidArgumentException
 * @return mixed new, typed object
 */
function recast($className, stdClass &$object)
{
    if (!class_exists($className))
        throw new InvalidArgumentException(sprintf('Inexistant class %s.', $className));

    $new = new $className();

    foreach($object as $property => &$value)
    {
        $new->$property = &$value;
        unset($object->$property);
    }
    unset($value);
    $object = (unset) $object;
    return $new;
}

Uso:

$array = array('h','n');

$obj=new stdClass;
$obj->action='auth';
$obj->params= &$array;
$obj->authKey=md5('i');

class RestQuery{
    public $action;
    public $params=array();
    public $authKey='';
}

$restQuery = recast('RestQuery', $obj);

var_dump($restQuery, $obj);

Salida:

object(RestQuery)#2 (3) {
  ["action"]=>
  string(4) "auth"
  ["params"]=>
  &array(2) {
    [0]=>
    string(1) "h"
    [1]=>
    string(1) "n"
  }
  ["authKey"]=>
  string(32) "865c0c0b4ab0e063e5caa3387c1a8741"
}
NULL

Esto es limitado debido al newoperador, ya que se desconocen qué parámetros necesitaría. Para su caso probablemente encaja.

hakre
fuente
1
Informar a otros que intenten utilizar este método. Hay una advertencia para esta función en el sentido de que la iteración sobre un objeto instanciado fuera de sí mismo no podrá establecer propiedades privadas o protegidas dentro del objeto convertido. EG: Configurando public $ authKey = ''; a privado $ authKey = ''; Resultados en E_ERROR: tipo 1 - No se puede acceder a la propiedad privada RestQuery :: $ authKey
fyrye
¿Sin embargo, una stdClass con propiedades privadas?
Frug
@Frug The OP indica específicamente este requisito ... emitir / convertir un objeto stdClass en un objeto completo de un tipo determinado
oucil
11

Tengo un problema muy similar. La solución de reflexión simplificada funcionó bien para mí:

public static function cast($destination, \stdClass $source)
{
    $sourceReflection = new \ReflectionObject($source);
    $sourceProperties = $sourceReflection->getProperties();
    foreach ($sourceProperties as $sourceProperty) {
        $name = $sourceProperty->getName();
        $destination->{$name} = $source->$name;
    }
    return $destination;
}
Sergei G
fuente
8

Espero que alguien encuentre esto útil

// new instance of stdClass Object
$item = (object) array(
    'id'     => 1,
    'value'  => 'test object',
);

// cast the stdClass Object to another type by passing
// the value through constructor
$casted = new ModelFoo($item);

// OR..

// cast the stdObject using the method
$casted = new ModelFoo;
$casted->cast($item);
class Castable
{
    public function __construct($object = null)
    {
        $this->cast($object);
    }

    public function cast($object)
    {
        if (is_array($object) || is_object($object)) {
            foreach ($object as $key => $value) {
                $this->$key = $value;
            }
        }
    }
} 
class ModelFoo extends Castable
{
    public $id;
    public $value;
}
Wizzard
fuente
¿Puede explicar por qué "is_array ($ object) || is_array ($ object)"?
Kukinsula
5

Función modificada para fundición profunda (usando recursividad)

/**
 * Translates type
 * @param $destination Object destination
 * @param stdClass $source Source
 */
private static function Cast(&$destination, stdClass $source)
{
    $sourceReflection = new \ReflectionObject($source);
    $sourceProperties = $sourceReflection->getProperties();
    foreach ($sourceProperties as $sourceProperty) {
        $name = $sourceProperty->getName();
        if (gettype($destination->{$name}) == "object") {
            self::Cast($destination->{$name}, $source->$name);
        } else {
            $destination->{$name} = $source->$name;
        }
    }
}
Jadrovski
fuente
2

Y otro enfoque más usando el patrón decorador y los getter & setters mágicos de PHP:

// A simple StdClass object    
$stdclass = new StdClass();
$stdclass->foo = 'bar';

// Decorator base class to inherit from
class Decorator {

    protected $object = NULL;

    public function __construct($object)
    {
       $this->object = $object;  
    }

    public function __get($property_name)
    {
        return $this->object->$property_name;   
    }

    public function __set($property_name, $value)
    {
        $this->object->$property_name = $value;   
    }
}

class MyClass extends Decorator {}

$myclass = new MyClass($stdclass)

// Use the decorated object in any type-hinted function/method
function test(MyClass $object) {
    echo $object->foo . '<br>';
    $object->foo = 'baz';
    echo $object->foo;   
}

test($myclass);
Benni
fuente
0

Por cierto: la conversión es muy importante si está serializado, principalmente porque la deserialización rompe el tipo de objetos y se convierte en stdclass, incluidos los objetos DateTime.

Actualicé el ejemplo de @Jadrovski, ahora permite objetos y matrices.

ejemplo

$stdobj=new StdClass();
$stdobj->field=20;
$obj=new SomeClass();
fixCast($obj,$stdobj);

matriz de ejemplo

$stdobjArr=array(new StdClass(),new StdClass());
$obj=array(); 
$obj[0]=new SomeClass(); // at least the first object should indicates the right class.
fixCast($obj,$stdobj);

código: (es recursivo). Sin embargo, no sé si es recursivo con matrices. Puede que le falte un is_array adicional

public static function fixCast(&$destination,$source)
{
    if (is_array($source)) {
        $getClass=get_class($destination[0]);
        $array=array();
        foreach($source as $sourceItem) {
            $obj = new $getClass();
            fixCast($obj,$sourceItem);
            $array[]=$obj;
        }
        $destination=$array;
    } else {
        $sourceReflection = new \ReflectionObject($source);
        $sourceProperties = $sourceReflection->getProperties();
        foreach ($sourceProperties as $sourceProperty) {
            $name = $sourceProperty->getName();
            if (is_object(@$destination->{$name})) {
                fixCast($destination->{$name}, $source->$name);
            } else {
                $destination->{$name} = $source->$name;
            }
        }
    }
}
magallanes
fuente
0

considere agregar un nuevo método a BusinessClass:

public static function fromStdClass(\stdClass $in): BusinessClass
{
  $out                   = new self();
  $reflection_object     = new \ReflectionObject($in);
  $reflection_properties = $reflection_object->getProperties();
  foreach ($reflection_properties as $reflection_property)
  {
    $name = $reflection_property->getName();
    if (property_exists('BusinessClass', $name))
    {
      $out->{$name} = $in->$name;
    }
  }
  return $out;
}

entonces puedes crear una nueva clase de negocio desde $ stdClass:

$converted = BusinessClass::fromStdClass($stdClass);
pgee70
fuente
0

Otro enfoque más.

Lo siguiente ahora es posible gracias a la reciente versión de PHP 7.

$theStdClass = (object) [
  'a' => 'Alpha',
  'b' => 'Bravo',
  'c' => 'Charlie',
  'd' => 'Delta',
];

$foo = new class($theStdClass)  {
  public function __construct($data) {
    if (!is_array($data)) {
      $data = (array) $data;
    }

    foreach ($data as $prop => $value) {
      $this->{$prop} = $value;
    }
  }
  public function word4Letter($letter) {
    return $this->{$letter};
  }
};

print $foo->word4Letter('a') . PHP_EOL; // Alpha
print $foo->word4Letter('b') . PHP_EOL; // Bravo
print $foo->word4Letter('c') . PHP_EOL; // Charlie
print $foo->word4Letter('d') . PHP_EOL; // Delta
print $foo->word4Letter('e') . PHP_EOL; // PHP Notice:  Undefined property

En este ejemplo, $ foo se inicializa como una clase anónima que toma una matriz o stdClass como único parámetro para el constructor.

Finalmente, recorremos cada elemento contenido en el objeto pasado y lo asignamos dinámicamente a la propiedad de un objeto.

Para hacer que este evento de aproximación sea más genérico, puede escribir una interfaz o un Trait que implementará en cualquier clase en la que desee poder emitir una stdClass.

asiby
fuente
0

Conviértalo en una matriz, devuelva el primer elemento de esa matriz y establezca el parámetro de retorno para esa clase. Ahora debería obtener el autocompletado para esa clase, ya que la regconizará como esa clase en lugar de stdclass.

/**
 * @return Order
 */
    public function test(){
    $db = new Database();

    $order = array();
    $result = $db->getConnection()->query("select * from `order` where productId in (select id from product where name = 'RTX 2070')");
    $data = $result->fetch_object("Order"); //returns stdClass
    array_push($order, $data);

    $db->close();
    return $order[0];
}
EMBRAGUE
fuente