Converte / esegue il cast di un oggetto stdClass in un'altra classe


89

Sto utilizzando un sistema di archiviazione di terze parti che mi restituisce solo oggetti stdClass indipendentemente da ciò che inserisco per qualche oscuro motivo. Quindi sono curioso di sapere se esiste un modo per eseguire il cast / convertire un oggetto stdClass in un oggetto completo di un determinato tipo.

Ad esempio qualcosa sulla falsariga di:

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

Sto solo eseguendo il casting di stdClass in un array e lo inserisco nel costruttore BusinessClass, ma forse c'è un modo per ripristinare la classe iniziale di cui non sono a conoscenza.

Nota: non sono interessato al tipo di risposte "Cambia il tuo sistema di archiviazione" poiché non è il punto di interesse. Consideratela più una questione accademica sulle capacità linguistiche.

Saluti


È spiegato nel mio post dopo lo pseudo codice di esempio. Sto eseguendo il casting in un array e sto alimentando un costruttore automatizzato.
The Mighty Rubber Duck

La risposta di @Adam Puza è molto migliore dell'hack mostrato nella risposta accettata. anche se sono sicuro che un mappatore sarebbe ancora il metodo preferito
Chris

Ebbene come svolge PDOStatement::fetchObjectquesto compito?
William Entriken

Risposte:


88

Vedi il manuale su Type Juggling sui possibili cast.

I cast consentiti sono:

  • (int), (integer) - cast a integer
  • (bool), (boolean) - cast in boolean
  • (float), (double), (real) - cast to float
  • (stringa) - cast su stringa
  • (array) - cast su array
  • (oggetto) - cast su oggetto
  • (non impostato) - cast a NULL (PHP 5)

Dovresti scrivere un Mapper che esegue il casting da stdClass a un'altra classe concreta. Non dovrebbe essere troppo difficile da fare.

Oppure, se sei di umore hacker, potresti adattare il seguente codice:

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

che pseudocast un array a un oggetto di una certa classe. Funziona prima serializzando l'array e quindi modificando i dati serializzati in modo che rappresenti una determinata classe. Il risultato non è quindi serializzato su un'istanza di questa classe. Ma come ho detto, è hacker, quindi aspettati effetti collaterali.

Da oggetto a oggetto, il codice sarebbe

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

9
Questo hack è una tecnica intelligente. Non lo userò poiché il mio modo attuale di risolvere il problema è più stabile, ma comunque interessante.
The Mighty Rubber Duck

1
Si finisce con un __PHP_Incomplete_Classoggetto utilizzando questo metodo (almeno a partire da PHP 5.6).
TiMESPLiNTER

1
@TiMESPLiNTER no, non lo fai. Vedi codepad.org/spGkyLzL . Assicurati che la classe a cui eseguire il cast sia stata inclusa prima di chiamare la funzione.
Gordon

@TiMESPLiNTER non è sicuro di cosa intendi. ha funzionato. è un esempio \ Foo ora.
Gordon

Sì, il problema è che aggiunge la proprietà stdClass. Quindi hai due fooBars (quello privato da example\Fooe quello pubblico da stdClass). Invece che sostituisce il valore.
TiMESPLiNTER

53

Puoi usare la funzione sopra per lanciare oggetti di classe non simili (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;
}

ESEMPIO:

class A 
{
  private $_x;   
}

class B 
{
  public $_x;   
}

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

$x = cast('A',$b);
$x = cast('B',$a);

5
È una soluzione piuttosto elegante , devo dire! Mi chiedo solo quanto bene scala ... La riflessione mi spaventa.
Theodore R. Smith

Ehi Adam, questa soluzione ha risolto un problema simile per me qui: stackoverflow.com/questions/35350585/… Se vuoi ottenere una risposta facile, vai avanti e lo controllerò. Grazie!
oucil

Non funziona per le proprietà ereditate dalle classi genitore.
Toilal

L'ho appena usato come base della mia soluzione per il seeding del mio database con ID noti per i test funzionali delle API con Behat. Il mio problema era che i miei ID normali sono UUID generati e non volevo aggiungere un metodo setId () nella mia entità solo per il bene del mio livello di test, e non volevo caricare i file delle fixtures e rallentare i test. Ora posso includere @Given the user :username has the id :idnella mia funzionalità e gestirla con riflessioni nella classe del contesto
nealio82

2
Ottima soluzione, voglio aggiungere che $destination = new $destination();può essere scambiata con $destination = ( new ReflectionClass( $destination ) )->newInstanceWithoutConstructor();se è necessario evitare di chiamare il costruttore.
Scuzzy

14

Per spostare tutte le proprietà esistenti di a stdClassin un nuovo oggetto con un nome di classe specificato:

/**
 * 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;
}

Utilizzo:

$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);

Produzione:

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

Questo è limitato a causa newdell'operatore in quanto non si sa di quali parametri avrebbe bisogno. Per il tuo caso probabilmente adatto.


1
Per informare gli altri che tentano di utilizzare questo metodo. C'è un avvertimento a questa funzione in quanto l'iterazione su un oggetto istanziato al di fuori di se stesso non sarà in grado di impostare proprietà private o protette all'interno dell'oggetto cast. ES: impostazione public $ authKey = ''; a $ authKey privato = ''; Risultati in E_ERROR: tipo 1 - Impossibile accedere alla proprietà privata RestQuery :: $ authKey
fyrye

Una stdClass con proprietà private però?
Frug

@Frug L'OP indica specificamente questo requisito ... cast / converte un oggetto stdClass in un oggetto a tutti gli effetti di un determinato tipo
oucil

11

Ho un problema molto simile. La soluzione di riflessione semplificata ha funzionato bene per me:

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;
}

8

Spero che qualcuno lo trovi utile

// 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;
}

Puoi spiegare perché "is_array ($ object) || is_array ($ object)"?
Kukinsula

5

Funzione modificata per il casting profondo (utilizzando la ricorsione)

/**
 * 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;
        }
    }
}

2

E ancora un altro approccio che utilizza il pattern decorator e il magic getter & setters di 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);

0

BTW: la conversione è molto importante se si è serializzati, principalmente perché la deserializzazione interrompe il tipo di oggetti e si trasforma in stdclass, inclusi gli oggetti DateTime.

Ho aggiornato l'esempio di @Jadrovski, ora consente oggetti e array.

esempio

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

array di esempio

$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);

codice: (è ricorsivo). Tuttavia, non so se è ricorsivo con gli array. Potrebbe mancare un is_array aggiuntivo

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;
            }
        }
    }
}

0

considera l'aggiunta di un nuovo metodo 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;
}

quindi puoi creare una nuova BusinessClass da $ stdClass:

$converted = BusinessClass::fromStdClass($stdClass);

0

Ancora un altro approccio.

Quanto segue è ora possibile grazie alla recente versione di 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

In questo esempio, $ foo viene inizializzato come una classe anonima che accetta un array o stdClass come unico parametro per il costruttore.

Alla fine, eseguiamo un ciclo tra gli elementi contenuti nell'oggetto passato e quindi assegniamo dinamicamente alla proprietà di un oggetto.

Per rendere questo evento di avvicinamento più generico, puoi scrivere un'interfaccia o un tratto che implementerai in qualsiasi classe in cui vuoi essere in grado di lanciare una stdClass.


0

Convertirlo in un array, restituire il primo elemento di quell'array e impostare il parametro return su quella classe. Ora dovresti ottenere il completamento automatico per quella classe in quanto lo registrerà come quella classe invece che come 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];
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.