Converter / lançar um objeto stdClass para outra classe

89

Estou usando um sistema de armazenamento de terceiros que só retorna objetos stdClass, não importa o que eu forneça por algum motivo obscuro. Portanto, estou curioso para saber se existe uma maneira de lançar / converter um objeto stdClass em um objeto completo de um determinado tipo.

Por exemplo, algo como:

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

Estou apenas lançando a stdClass em uma matriz e alimentando-a para o construtor BusinessClass, mas talvez haja uma maneira de restaurar a classe inicial da qual não estou ciente.

Observação: não estou interessado no tipo de respostas 'Alterar seu sistema de armazenamento', pois não é o ponto de interesse. Por favor, considere isso mais uma questão acadêmica sobre as capacidades linguísticas.

Felicidades

O poderoso pato de borracha
fonte
Isso é explicado em minha postagem após o exemplo do pseudo código. Estou lançando em uma matriz e alimentando um construtor automatizado.
The Mighty Rubber Duck
A resposta de @Adam Puza é muito melhor do que o hack mostrado na resposta aceita. embora eu tenha certeza de que um mapeador ainda seria o método preferido
Chris
Bem, como PDOStatement::fetchObjectrealizar essa tarefa?
William Entriken

Respostas:

88

Veja o manual sobre Malabarismo de Tipos sobre possíveis elencos.

Os lançamentos permitidos são:

  • (int), (inteiro) - convertido em inteiro
  • (bool), (booleano) - converter em booleano
  • (flutuante), (duplo), (real) - convertido para flutuante
  • (string) - lançar em string
  • (array) - lançar em array
  • (objeto) - lançar em objeto
  • (não definido) - convertido para NULL (PHP 5)

Você teria que escrever um Mapeador que faça a conversão de stdClass para outra classe concreta. Não deve ser muito difícil de fazer.

Ou, se você estiver com um humor hackeado, pode adaptar o seguinte código:

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

que pseudocats um array para um objeto de uma determinada classe. Isso funciona primeiro serializando a matriz e, em seguida, alterando os dados serializados para que representem uma determinada classe. O resultado é desserializado para uma instância desta classe então. Mas, como eu disse, é hackeado, então espere efeitos colaterais.

Para objeto para objeto, o código seria

function objectToObject($instance, $className) {
    return unserialize(sprintf(
        'O:%d:"%s"%s',
        strlen($className),
        $className,
        strstr(strstr(serialize($instance), '"'), ':')
    ));
}
Gordon
fonte
9
Este hack é uma técnica inteligente. Não o usarei, pois minha maneira atual de resolver o problema é mais estável, mas interessante.
The Mighty Rubber Duck
1
Você acaba com um __PHP_Incomplete_Classobjeto usando este método (pelo menos a partir do PHP 5.6).
TiMESPLiNTER
1
@TiMESPLiNTER não, não precisa. Consulte codepad.org/spGkyLzL . Certifique-se de que a classe a ser convertida foi incluída antes de chamar a função.
Gordon
@TiMESPLiNTER não tenho certeza do que você quer dizer. funcionou. é um exemplo \ Foo agora.
Gordon
Sim, o problema é que acrescenta a propriedade stdClass. Portanto, você tem dois fooBars (o privado de example\Fooe o público de stdClass). Em vez disso, ele substitui o valor.
TiMESPLiNTER
53

Você pode usar a função acima para lançar objetos de classe não semelhantes (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;
}

EXEMPLO:

class A 
{
  private $_x;   
}

class B 
{
  public $_x;   
}

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

$x = cast('A',$b);
$x = cast('B',$a);
Adam Puza
fonte
5
É uma solução muito elegante , devo dizer! Eu só me pergunto o quão bem ele escala ... A reflexão me assusta.
Theodore R. Smith
Ei Adam, esta solução resolveu um problema semelhante para mim aqui: stackoverflow.com/questions/35350585/… Se você quiser obter uma resposta fácil, vá em frente e eu verificarei. Obrigado!
oucil
Não funciona para propriedades herdadas de classes pai.
Toilal
Acabei de usar isso como base para minha solução para semear meu banco de dados com IDs conhecidos para teste funcional de API com Behat. Meu problema era que meus IDs normais são UUIDs gerados e eu não queria adicionar um método setId () em minha entidade apenas por causa da minha camada de teste, e eu não queria carregar arquivos de fixtures e desacelerar os testes. Agora posso incluir @Given the user :username has the id :idno meu recurso e tratá-lo com reflexos na classe de contexto
nealio82
2
Ótima solução, eu quero adicionar que $destination = new $destination();pode ser trocado $destination = ( new ReflectionClass( $destination ) )->newInstanceWithoutConstructor();se você precisar evitar chamar o construtor.
Scuzzy
14

Para mover todas as propriedades existentes de a stdClasspara um novo objeto de um nome de classe 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);

Resultado:

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

Isso é limitado por causa do newoperador, pois não se sabe quais parâmetros seriam necessários. Provavelmente adequado para o seu caso.

hakre
fonte
1
Para informar outras pessoas que tentam usar este método. Há uma advertência para essa função, pois a iteração sobre um objeto instanciado fora de si mesmo não será capaz de definir propriedades privadas ou protegidas dentro do objeto fundido. EX .: Configurando public $ authKey = ''; para private $ authKey = ''; Resultados em E_ERROR: tipo 1 - Não é possível acessar a propriedade privada RestQuery :: $ authKey
fyrye
Uma stdClass com propriedades privadas?
Frug
@Frug O OP indica especificamente este requisito ... converter / converter um objeto stdClass em um objeto completo de um determinado tipo
oucil
11

Eu tenho um problema muito similar. A solução de reflexão simplificada funcionou bem para mim:

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
fonte
8

Espero que alguém ache isso ú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
fonte
Você pode explicar por que "is_array ($ object) || is_array ($ object)"?
Kukinsula
5

Função alterada para deep casting (usando recursão)

/**
 * 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
fonte
2

E ainda outra abordagem usando o padrão decorator e getter e setters mágicos do 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
fonte
0

BTW: a conversão é muito importante se você for serializado, principalmente porque a desserialização quebra o tipo de objetos e se transforma em stdclass, incluindo objetos DateTime.

Atualizei o exemplo do @Jadrovski, agora ele permite objetos e arrays.

exemplo

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

array de exemplo

$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: (é recursivo). No entanto, não sei se é recursivo com matrizes. Pode estar faltando um is_array extra

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;
            }
        }
    }
}
Magalhães
fonte
0

considere adicionar um novo método ao 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;
}

então você pode fazer uma nova BusinessClass a partir de $ stdClass:

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

Mais uma abordagem.

O seguinte agora é possível graças à versão recente do 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

Neste exemplo, $ foo está sendo inicializado como uma classe anônima que recebe um array ou stdClass como único parâmetro para o construtor.

Por fim, percorremos cada um dos itens contidos no objeto passado e os atribuímos dinamicamente à propriedade de um objeto.

Para tornar este evento Approch mais genérico, você pode escrever uma interface ou uma Característica que irá implementar em qualquer classe onde você deseja ser capaz de lançar uma stdClass.

asiby
fonte
0

Converta-o em uma matriz, retorne o primeiro elemento dessa matriz e defina o parâmetro de retorno para essa classe. Agora você deve obter o preenchimento automático para essa classe, já que ele irá regconizá-la como essa classe em vez 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];
}
CLUTCHER
fonte