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

add a xsd2php cli command #72

Closed staabm closed 8 years ago

staabm commented 8 years ago

Since I started using the new value-object feature for our contract first webservices I get into the situation where I have to manually define a lot of php value objects, which map 1:1 via the new feature a corresponding XSD.

What do you think about adding a new cli command to create/generate php files from a xsd, in a form that they are 1:1 consumable using the new feature?

I already googled for an hour but wasnt able to find something suitable which isnt part of a huge component or library.

I thought about a very small script, in which you feed in a XSD and creates classes from xsd complexTypes ,interprets min/maxOccurs and some basic types like string, long, etc. and adds some docblocks for metainfos which we cannot typehint in php5.5+

at best we would have a lib which does this things already, but I could not find one right now.

staabm commented 8 years ago

Eating our own dogfood I already got something which works for the basic use-case:

#!/usr/bin/env php
<?php

use Sabre\Xml\Reader;

require __DIR__ .'/../../../../../vendor/autoload.php';

$srcFolder = $argv[1];

if (!is_readable($srcFolder)) {
    throw new Exception('folder "'. $srcFolder .'" is not readable');
}

$reader = new Reader();
$reader->elementMap = array (
    '{http://www.w3.org/2001/XMLSchema}element' => function(Reader $reader) {
        $element = new XsdElement();
        if ($name = $reader->getAttribute('name')) {
            $element->name = $name;
        }
        if ($type = $reader->getAttribute('type')) {
            $element->type = $type;
        }

        $children = $reader->parseInnerTree();
        if (is_array($children)) {
            foreach($children as $child) {
                if ($child['value'] instanceof XsdComplexType) {
                    $element->complexType = $child['value'];
                }
            }
        }

        return $element;
    },
    '{http://www.w3.org/2001/XMLSchema}complexType' => function(Reader $reader) {
        $complexType = new XsdComplexType();
        if ($name = $reader->getAttribute('name')) {
            $complexType->name = $name;
        }

        $children = $reader->parseInnerTree();
        foreach($children as $child) {
            if ($child['value'] instanceof XsdSequence) {
                $complexType->sequence = $child['value'];
            }
        }
        return $complexType;
    },
    '{http://www.w3.org/2001/XMLSchema}sequence' => function(Reader $reader) {
        $sequence = new XsdSequence();

        $children = $reader->parseInnerTree();
        foreach($children as $child) {
            if ($child['value'] instanceof XsdElement) {
                $sequence->elements[] = $child['value'];
            }
        }

        return $sequence;
    }
);

$files = rglob($srcFolder. '/*.xsd');
foreach($files as $xsd) {
    // FIXME: filter a fixed XSD for now
    if (strpos($xsd, 'order_response.xsd') === false) continue;
    echo $xsd."\n";

    $reader->xml(file_get_contents($xsd));
    $tree = $reader->parse();
    print_r($tree);
}

// defines "instances" which actually can be used in XML files
class XsdElement{
    /**
     * @var string
     */
    public $name;
    /**
     * @var string
     */
    public $type;
    /**
     * @var XsdComplexType|null
     */
    public $complexType;
}

// defines a type, meant for re-use across the XSD
class XsdComplexType {
    /**
     * @var string
     */
    public $name;
    /**
     * @var XsdSequence
     */
    public $sequence;
}

// defines a list or properties with a fixed order
class XsdSequence {
    /**
     * @var XsdElement[]
     */
    public $elements = array();
}

class ValueObjectClass {
    /**
     * @var string
     */
    public $name;
    /**
     * @var string
     */
    public $namespace;
    /**
     * property-name => property-type
     *
     * @var array
     */
    public $properties = array();
}

// stolen from http://www.westhost.com/contest/php/function/rglob/123
function rglob($pattern='*', $flags = 0, $path=false)
{
    if (!$path) { $path=dirname($pattern).DIRECTORY_SEPARATOR; }
    $pattern=basename($pattern);
    $paths=glob($path.'*', GLOB_MARK|GLOB_ONLYDIR|GLOB_NOSORT);
    $files=glob($path.$pattern, $flags);
    foreach ($paths as $path) {
        $files=array_merge($files,rglob($pattern, $flags, $path));
    }
    return $files;
}

which produces

./generator.php entities/
entities/order_response.xsd
Array
(
    [name] => {http://www.w3.org/2001/XMLSchema}schema
    [value] => Array
        (
            [0] => Array
                (
                    [name] => {http://www.w3.org/2001/XMLSchema}element
                    [value] => XsdElement Object
                        (
                            [name] => order_response
                            [type] =>
                            [complexType] => XsdComplexType Object
                                (
                                    [name] =>
                                    [sequence] => XsdSequence Object
                                        (
                                            [elements] => Array
                                                (
                                                    [0] => XsdElement Object
                                                        (
                                                            [name] => request_id
                                                            [type] => xs:string
                                                            [complexType] =>
                                                        )

                                                    [1] => XsdElement Object
                                                        (
                                                            [name] => order_id
                                                            [type] => xs:string
                                                            [complexType] =>
                                                        )

                                                    [2] => XsdElement Object
                                                        (
                                                            [name] => status
                                                            [type] => xs:string
                                                            [complexType] =>
                                                        )

                                                    [3] => XsdElement Object
                                                        (
                                                            [name] => version
                                                            [type] => xs:int
                                                            [complexType] =>
                                                        )

                                                    [4] => XsdElement Object
                                                        (
                                                            [name] => success
                                                            [type] => xs:boolean
                                                            [complexType] =>
                                                        )

                                                    [5] => XsdElement Object
                                                        (
                                                            [name] => error_detail
                                                            [type] => errorDetailType
                                                            [complexType] =>
                                                        )

                                                )

                                        )

                                )

                        )

                    [attributes] => Array
                        (
                            [name] => order_response
                        )

                )

            [1] => Array
                (
                    [name] => {http://www.w3.org/2001/XMLSchema}complexType
                    [value] => XsdComplexType Object
                        (
                            [name] => errorDetailType
                            [sequence] => XsdSequence Object
                                (
                                    [elements] => Array
                                        (
                                            [0] => XsdElement Object
                                                (
                                                    [name] => error_code
                                                    [type] => xs:string
                                                    [complexType] =>
                                                )

                                            [1] => XsdElement Object
                                                (
                                                    [name] => error_message
                                                    [type] => xs:string
                                                    [complexType] =>
                                                )

                                        )

                                )

                        )

                    [attributes] => Array
                        (
                            [name] => errorDetailType
                        )

                )

        )

    [attributes] => Array
        (
            [targetNamespace] => http://clxERP/order
            [elementFormDefault] => qualified
        )

)

for this XSD

<?xml version="1.0" encoding="ISO-8859-1"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns="http://clxERP/order"
           targetNamespace="http://clxERP/order"
           elementFormDefault="qualified">
  <xs:element name="order_response">
      <xs:complexType>
        <xs:sequence>
            <xs:element name="request_id" type="xs:string" />
            <xs:element name="order_id" type="xs:string" />
            <xs:element name="status" type="xs:string" />
            <xs:element name="version" type="xs:int" />
            <xs:element name="success" type="xs:boolean" />
            <xs:element name="error_detail" type="errorDetailType" />
        </xs:sequence>
      </xs:complexType>
  </xs:element>

  <xs:complexType name="errorDetailType">
      <xs:sequence>
           <xs:element name="error_code" type="xs:string"/>
           <xs:element name="error_message" type="xs:string"/>
       </xs:sequence>
  </xs:complexType>
</xs:schema>    

one problem right now: the property error_detail references another complexType within the xsd instead of a "scalar" type like xs:string. I am not yet sure how to parse a xs:element's type attribute and determine whether it is pointing to another complexType or it is just a primitive.

any idea on that?

btw: playing with sabre/xml in more use-cases gave me the impression I am using the right lib. It was really easy to get to the point where I am right now. :rocket: :tada:

evert commented 8 years ago

I am a little worried honestly that the amount of xml documents we can actually parse with this is too low. scalars work fine, but I imagine that most are simply more complex than that.

I don't feel super comfortable adding a potentially complicated feature that people will not be able to use as soon as they go beyond super-simple key-value.

One thing I would really like though, is to create separate composer packages that ship standard serializer/deserializers for popular xml formats... such as xmlschema (sabre/xml-xmlschema), and atom (sabre/xml-atom) and eventually a separate package for all DAV (de-)serializers, etc... I think those could be super useful and it would create a bit of an ecosystem of xml parsing packages.

However, if you think I'm wrong, I'd also love to hear it... I'm also coming in here from a perspective that's "I don't think I can use this feature myself, so I think others won't either".

staabm commented 8 years ago

I am not yet sure if this is generic enough for use in the lib. i will finish the command nevertheless for my "specific" use case, so it wont be lost or integrated later on, after we used it in several scenarios.

I agree that having some kind of serializer libs, which are more or less, lib independent could create a eco-system for itself.

I will finish my command tomorrow and maybe come back to this after getting more in touch with it. Thanks for your opinion.