sabre-io / xml

sabre/xml is an XML library that you may not hate.
http://sabre.io/xml/
BSD 3-Clause "New" or "Revised" License
516 stars 77 forks source link

External object serializers. #61

Closed evert closed 8 years ago

evert commented 8 years ago

cc: @staabm. What do you think?

staabm commented 8 years ago

Any chanche we can get the classmap beeing used for reading and writing?

evert commented 8 years ago

That wouldn't make a lot of sense, because during reading we don't know yet which class needs to be created. All we have is the element name.

staabm commented 8 years ago

Oh, obviously you are right ;-)

staabm commented 8 years ago

one thing which bugs me...

class ErpOrderService extends Sabre\Xml\Service {
    public $elementMap = array(
        '{http://b2bOrder.complex.de}order_response' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
            return $popoDeserializer($reader, new OrderResponse(), XMLNS_ORDER);
        },
        '{http://b2bOrder.complex.de}order_status' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderStatus(), XMLNS_ORDER);
        },
    );
}

is not valid php, as I cannot use closures on the public field here

staabm commented 8 years ago

ok, wil change it to

$erpOrderService = new Sabre\Xml\Service();
$erpOrderService->elementMap = array(
    '{'. XMLNS_ORDER .'}order_response' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderResponse(), XMLNS_ORDER);
    },
    '{'. XMLNS_ORDER .'}order_status' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderStatus(), XMLNS_ORDER);
    },
);

like the Reader/Writer examples

staabm commented 8 years ago

... progress...

$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <order_response xmlns="http://b2bOrder.complex.de">
 <request_id>4711</request_id>
 <order_id>54752</order_id>
    <order_status xmlns="http://b2bOrder.complex.de">
         <status>INITIALIZED</status>
    </order_status>
 <success>true</success>
 </order_response>';

$popoDeserializer = function(Sabre\Xml\Reader $reader, $popo, $namespace)
{
    if ($reader->isEmptyElement) {
        throw new Exception(__CLASS__ . ' does not support empty elements');
    }

    $reader->read();
    do {

        if ($reader->nodeType === Sabre\Xml\Reader::ELEMENT && $reader->namespaceURI == $namespace) {

            $popo->{$reader->localName} = $reader->parseCurrentElement()['value'];
        } else {
            $reader->read();
        }
    } while ($reader->nodeType !== Sabre\Xml\Reader::END_ELEMENT);

    $reader->read();

    return $popo;
};

$popoSerializer = function(Sabre\Xml\Writer $writer, $popo, $namespace)  {
    foreach(get_object_vars($popo) as $key => $val) {
        $writer->writeElement('{'. $namespace .'}' . $key, $val);
    }
};

class OrderResponse
{
    public $request_id;
    public $order_id;
    public $order_status;
    public $success;
}

class OrderStatus
{
    public $status;
}

define("XMLNS_ORDER", "http://b2bOrder.complex.de");

$erpOrderService = new Sabre\Xml\Service();
$erpOrderService->namespaceMap[XMLNS_ORDER] = '';
// how to read xml into objects
$erpOrderService->elementMap = array(
    '{'. XMLNS_ORDER .'}order_response' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderResponse(), XMLNS_ORDER);
    },
    '{'. XMLNS_ORDER .'}order_status' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderStatus(), XMLNS_ORDER);
    },
);
// how to create xml from obejects
$erpOrderService->classMap = array(
    'OrderResponse' => function(Sabre\Xml\Writer $writer, $orderResponse) use ($popoSerializer) {
        /** @var $orderResponse OrderResponse */
        $popoSerializer($writer, $orderResponse, XMLNS_ORDER);
    },
    'OrderStatus' => function(Sabre\Xml\Writer $writer, $orderStatus) use ($popoSerializer) {
        /** @var $orderStatus OrderStatus */
        $popoSerializer($writer, $orderStatus, XMLNS_ORDER);
    }
);
var_dump($xml);
$orderResp = $erpOrderService->parse($xml);
var_dump($orderResp);
var_dump($erpOrderService->write(get_class($orderResp), $orderResp));

output

string(331) "<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <order_response xmlns="http://b2bOrder.complex.de">
 <request_id>4711</request_id>
 <order_id>54752</order_id>
    <order_status xmlns="http://b2bOrder.complex.de">
         <status>INITIALIZED</status>
    </order_status>
 <success>true</success>
 </order_response>"
object(OrderResponse)#11 (4) {
  ["request_id"]=>
  string(4) "4711"
  ["order_id"]=>
  string(5) "54752"
  ["order_status"]=>
  object(OrderStatus)#12 (1) {
    ["status"]=>
    string(11) "INITIALIZED"
  }
  ["success"]=>
  string(4) "true"
}
string(248) "<?xml version="1.0"?>
<OrderResponse xmlns="http://b2bOrder.complex.de">
 <:request_id>4711</:request_id>
 <:order_id>54752</:order_id>
 <:order_status>
  <:status>INITIALIZED</:status>
 </:order_status>
 <:success>true</:success>
</OrderResponse>
"

except the : in the written xml, it looks fine ;)

In contrast to your usual convention that a element should not emit the xml-element for itself, in case of having a external deserializer it would make sense to have the serializer emit the whole element... wdyt? (in case we agree here, I cannot use the service->write as the root element would then be emitted twice

evert commented 8 years ago

as I cannot use closures on the public field here

Alternatively you could also have set them in the constructor.

except the : in the written xml, it looks fine ;)

What's with that :? That doesn't look right!

in case of having a external deserializer it would make sense to have the serializer emit the whole element... wdyt? (in case we agree here, I cannot use the service->write as the root element would then be emitted twice

Personally I would not break the rule, but if you think you have good reasons to do so, you should ;)

staabm commented 8 years ago

What's with that :? That doesn't look right!

I dont know why. The above code is runnable, maybe it helps to repro the problem

staabm commented 8 years ago

I guess the : are caused by the line $erpOrderService->namespaceMap[XMLNS_ORDER] = ''; (not tested yet).

I do this to not get a namespace (inspired by the docs from http://sabre.io/xml/writing/)

evert commented 8 years ago

Can you try null instead of '' ?

staabm commented 8 years ago

@evert you are right, we need null instead of ''.

send a PR which fixes the example code. https://github.com/fruux/sabre.io/pull/61

It also fixes another error with the code.

So after all this we have a working code:

$xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <order_response xmlns="http://b2bOrder.complex.de">
 <request_id>4711</request_id>
 <order_id>54752</order_id>
    <order_status xmlns="http://b2bOrder.complex.de">
         <status>INITIALIZED</status>
    </order_status>
 <success>true</success>
 </order_response>';

$popoDeserializer = function(Sabre\Xml\Reader $reader, $popo, $namespace)
{
    if ($reader->isEmptyElement) {
        throw new Exception(__CLASS__ . ' does not support empty elements');
    }

    $reader->read();
    do {

        if ($reader->nodeType === Sabre\Xml\Reader::ELEMENT && $reader->namespaceURI == $namespace) {

            $popo->{$reader->localName} = $reader->parseCurrentElement()['value'];
        } else {
            $reader->read();
        }
    } while ($reader->nodeType !== Sabre\Xml\Reader::END_ELEMENT);

    $reader->read();

    return $popo;
};

$popoSerializer = function(Sabre\Xml\Writer $writer, $popo, $namespace)  {
    foreach(get_object_vars($popo) as $key => $val) {
        $writer->writeElement('{'. $namespace .'}' . $key, $val);
    }
};

class OrderResponse
{
    public $request_id;
    public $order_id;
    public $order_status;
    public $success;
}

class OrderStatus
{
    public $status;
}

define("XMLNS_ORDER", "http://b2bOrder.complex.de");

$erpOrderService = new Sabre\Xml\Service();
$erpOrderService->namespaceMap[XMLNS_ORDER] = null;
// how to read xml into objects
$erpOrderService->elementMap = array(
    '{'. XMLNS_ORDER .'}order_response' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderResponse(), XMLNS_ORDER);
    },
    '{'. XMLNS_ORDER .'}order_status' => function (Sabre\Xml\Reader $reader) use ($popoDeserializer) {
        return $popoDeserializer($reader, new OrderStatus(), XMLNS_ORDER);
    },
);
// how to create xml from obejects
$erpOrderService->classMap = array(
    'OrderResponse' => function(Sabre\Xml\Writer $writer, $orderResponse) use ($popoSerializer) {
        /** @var $orderResponse OrderResponse */
        $popoSerializer($writer, $orderResponse, XMLNS_ORDER);
    },
    'OrderStatus' => function(Sabre\Xml\Writer $writer, $orderStatus) use ($popoSerializer) {
        /** @var $orderStatus OrderStatus */
        $popoSerializer($writer, $orderStatus, XMLNS_ORDER);
    }
);

echo "<xmp>";
var_dump($xml);
$orderResp = $erpOrderService->parse($xml);
var_dump($orderResp);
var_dump($erpOrderService->write(get_class($orderResp), $orderResp));

output

string(323) "<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <order_response xmlns="http://b2bOrder.complex.de">
 <request_id>4711</request_id>
 <order_id>54752</order_id>
    <order_status xmlns="http://b2bOrder.complex.de">
         <status>INITIALIZED</status>
    </order_status>
 <success>true</success>
 </order_response>"
object(OrderResponse)#10 (4) {
  ["request_id"]=>
  string(4) "4711"
  ["order_id"]=>
  string(5) "54752"
  ["order_status"]=>
  object(OrderStatus)#11 (1) {
    ["status"]=>
    string(11) "INITIALIZED"
  }
  ["success"]=>
  string(4) "true"
}
string(238) "<?xml version="1.0"?>
<OrderResponse xmlns="http://b2bOrder.complex.de">
 <request_id>4711</request_id>
 <order_id>54752</order_id>
 <order_status>
  <status>INITIALIZED</status>
 </order_status>
 <success>true</success>
</OrderResponse>
"

do you think this kind of popo serializer/deserializer is something which should be shipped with sabre/xml or is it to specific for my current app?

evert commented 8 years ago

I would definitely think it's a good idea to have this as a core feature btw!

evert commented 8 years ago

Perhaps we can integrate this in the Service class with a function that takes a class name and xml element name and automatically registers both the serializer and deserializer.

staabm commented 8 years ago

Agree that having it in the Service class is a good idea. Its the place where this kind of api-sugar should happen. PR incoming