Convertir / convertir un objet stdClass en une autre classe

89

J'utilise un système de stockage tiers qui ne me renvoie que des objets stdClass, peu importe ce que je nourris pour une raison obscure. Je suis donc curieux de savoir s'il existe un moyen de convertir / convertir un objet stdClass en un objet à part entière d'un type donné.

Par exemple, quelque chose du genre:

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

Je suis juste en train de convertir la classe stdClass dans un tableau et de la transmettre au constructeur BusinessClass, mais il existe peut-être un moyen de restaurer la classe initiale dont je ne suis pas au courant.

Remarque: je ne suis pas intéressé par le type de réponses «Changer votre système de stockage» car ce n'est pas le point d'intérêt. S'il vous plaît, considérez-le plus comme une question académique sur les capacités linguistiques.

À votre santé

Le puissant canard en caoutchouc
la source
C'est expliqué dans mon article après l'exemple de pseudo code. Je jette dans un tableau et alimente un constructeur automatisé.
The Mighty Rubber Duck
La réponse de @Adam Puza est bien meilleure que le hack montré dans la réponse acceptée. même si je suis sûr qu'un mappeur serait toujours la méthode préférée
Chris
Eh bien, comment PDOStatement::fetchObjectaccomplir cette tâche?
William Entriken

Réponses:

88

Voir le manuel sur la jonglerie de types sur les lancers possibles.

Les lancers autorisés sont:

  • (int), (integer) - cast en entier
  • (booléen), (boolean) - cast en booléen
  • (float), (double), (real) - cast to float
  • (chaîne) - cast en chaîne
  • (array) - transtyper en tableau
  • (objet) - transtyper en objet
  • (unset) - cast en NULL (PHP 5)

Vous devrez écrire un mappeur qui effectue le transtypage de stdClass vers une autre classe concrète. Cela ne devrait pas être trop difficile à faire.

Ou, si vous êtes d'humeur hackish, vous pouvez adapter le code suivant:

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

qui pseudo-diffuse un tableau en un objet d'une certaine classe. Cela fonctionne en sérialisant d'abord le tableau, puis en modifiant les données sérialisées afin qu'elles représentent une certaine classe. Le résultat est alors désérialisé à une instance de cette classe. Mais comme je l'ai dit, c'est hackish, alors attendez-vous à des effets secondaires.

Pour objet à objet, le code serait

function objectToObject($instance, $className) {
    return unserialize(sprintf(
        'O:%d:"%s"%s',
        strlen($className),
        $className,
        strstr(strstr(serialize($instance), '"'), ':')
    ));
}
Gordon
la source
9
Ce hack est une technique intelligente. Je ne l'utiliserai pas car ma façon actuelle de résoudre le problème est plus stable, mais néanmoins intéressante.
The Mighty Rubber Duck
1
Vous vous retrouvez avec un __PHP_Incomplete_Classobjet en utilisant cette méthode (au moins à partir de PHP 5.6).
TiMESPLiNTER
1
@TiMESPLiNTER non, vous ne le faites pas. Voir codepad.org/spGkyLzL . Assurez-vous que la classe vers laquelle effectuer un cast a été incluse avant d'appeler la fonction.
Gordon
@TiMESPLiNTER ne sais pas ce que vous voulez dire. ça a marché. c'est un exemple \ Foo maintenant.
Gordon
Ouais, le problème est qu'il ajoute la propriété stdClass. Vous avez donc deux fooBars (le privé de example\Fooet le public de stdClass). Au lieu de cela, il remplace la valeur.
TiMESPLiNTER
53

Vous pouvez utiliser la fonction ci-dessus pour convertir des objets de classe non similaires (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;
}

EXEMPLE:

class A 
{
  private $_x;   
}

class B 
{
  public $_x;   
}

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

$x = cast('A',$b);
$x = cast('B',$a);
Adam Puza
la source
5
C'est une solution assez élégante , je dois dire! Je me demande juste à quel point ça évolue ... La réflexion me fait peur.
Theodore R. Smith
Hey Adam, cette solution a résolu un problème similaire pour moi ici: stackoverflow.com/questions/35350585/... Si vous voulez obtenir une réponse facile, allez-y et je vais le cocher. Merci!
oucil
Cela ne fonctionne pas pour les propriétés héritées des classes parentes.
Toilal
Je viens de l'utiliser comme base de ma solution pour amorcer ma base de données avec des ID connus pour les tests fonctionnels d'API avec Behat. Mon problème était que mes ID normaux sont des UUID générés et je ne voulais pas ajouter de méthode setId () dans mon entité juste pour le bien de ma couche de test, et je ne voulais pas charger les fichiers de fixtures et ralentir les tests. Maintenant, je peux l'inclure @Given the user :username has the id :iddans ma fonctionnalité et la gérer avec des réflexions dans la classe de contexte
nealio82
2
Excellente solution, je veux ajouter qui $destination = new $destination();peut être échangée $destination = ( new ReflectionClass( $destination ) )->newInstanceWithoutConstructor();si vous devez éviter d'appeler le constructeur.
Scuzzy
14

Pour déplacer toutes les propriétés existantes de a stdClassvers un nouvel objet d'un nom de classe spécifié:

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

Usage:

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

Production:

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

Ceci est limité en raison de l' newopérateur car on ne sait pas de quels paramètres il aurait besoin. Pour votre cas, probablement approprié.

hakre
la source
1
Pour informer les autres qui tentent d'utiliser cette méthode. Il y a une mise en garde à cette fonction en ce que l'itération sur un objet instancié en dehors de lui-même ne sera pas en mesure de définir des propriétés privées ou protégées dans l'objet transtypé. EG: Définition de public $ authKey = ''; à private $ authKey = ''; Résultats dans E_ERROR: type 1 - Impossible d'accéder à la propriété privée RestQuery :: $ authKey
fyrye
Un stdClass avec des propriétés privées?
Frug
@Frug L'OP indique spécifiquement cette exigence ... transtyper / convertir un objet stdClass en un objet à part entière d'un type donné
oucil
11

J'ai un problème très similaire. La solution de réflexion simplifiée a très bien fonctionné pour moi:

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
la source
8

J'espère que quelqu'un trouvera cela 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;
}
Wizzard
la source
Pouvez-vous expliquer pourquoi "is_array ($ object) || is_array ($ object)"?
kukinsula
5

Fonction modifiée pour le casting profond (en utilisant la récursivité)

/**
 * 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
la source
2

Et encore une autre approche utilisant le motif décorateur et les getter & setters magiques PHPs:

// 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
la source
0

BTW: La conversion est très importante si vous êtes sérialisé, principalement parce que la désérialisation rompt le type d'objets et se transforme en stdclass, y compris les objets DateTime.

J'ai mis à jour l'exemple de @Jadrovski, maintenant il autorise les objets et les tableaux.

exemple

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

exemple de tableau

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

code: (c'est récursif). Cependant, je ne sais pas si c'est récursif avec des tableaux. Peut-être qu'il manque un is_array supplémentaire

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
la source
0

envisagez d'ajouter une nouvelle méthode à 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;
}

alors vous pouvez créer une nouvelle BusinessClass à partir de $ stdClass:

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

Encore une autre approche.

Ce qui suit est désormais possible grâce à la récente version 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

Dans cet exemple, $ foo est en cours d'initialisation en tant que classe anonyme qui prend un tableau ou stdClass comme seul paramètre pour le constructeur.

Finalement, nous parcourons chacun des éléments contenus dans l'objet passé et attribuons ensuite dynamiquement la propriété d'un objet.

Pour rendre cet événement d'approche plus générique, vous pouvez écrire une interface ou un trait que vous implémenterez dans n'importe quelle classe où vous souhaitez pouvoir lancer un stdClass.

asiby
la source
0

Convertissez-le en tableau, retournez le premier élément de ce tableau et définissez le paramètre de retour sur cette classe. Vous devriez maintenant obtenir la saisie semi-automatique pour cette classe car elle la regconisera en tant que classe au lieu 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];
}
EMBRAYAGE
la source