Skip to content

Commit

Permalink
Document (most of) the ClassGenerator
Browse files Browse the repository at this point in the history
  • Loading branch information
Garethp committed Aug 25, 2024
1 parent 4dd5f37 commit 245c1e8
Showing 1 changed file with 164 additions and 29 deletions.
193 changes: 164 additions & 29 deletions src/Generator/ClassGenerator.php
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
<?php
/**
* Created by PhpStorm.
* User: gareth
* Date: 7-8-15
* Time: 15:51
*/

namespace garethp\ews\Generator;

use garethp\ews\API\Enumeration;
use Goetas\Xsd\XsdToPhp\Php\Structure\PHPClassOf;
use phpDocumentor\Reflection\DocBlock\Tag\VarTag;
use Zend\Code\Generator;
use Goetas\Xsd\XsdToPhp\Php\Structure\PHPClass;
use Zend\Code\Generator\DocBlockGenerator;
use Zend\Code\Generator\PropertyGenerator;
use Goetas\Xsd\XsdToPhp\Php\Structure\PHPProperty;
use Doctrine\Common\Inflector\Inflector;

class ClassGenerator extends \Goetas\Xsd\XsdToPhp\Php\ClassGenerator
/**
* This whole class acts as our main generator for our Types and Messages from the xsd specifications. It's responsible
* for creating the class, properties and methods that exist within the class. The list of things that we attempt to
* handle are:
*
* * Set the class to extend from either the Type/Message that already exists or a base class that we've created.
* * For each property in the xsd schema, we create a property in the class.
* * For each property in the xsd schema that is a Date, DateTime or Time method we add the property to a _typeMap
* * For each property in the xsd schema, we create a getter and setter method.
* * For each boolean property in the xsd schema, we create an is method.
* * For each array property in the xsd schema, we create an adder method.
*/
class ClassGenerator
{
public function fixInterfaces(Generator\ClassGenerator $class)
public function fixInterfaces(Generator\ClassGenerator $class): Generator\ClassGenerator
{
$interfaces = $class->getImplementedInterfaces();

Expand All @@ -37,6 +42,11 @@ public function generate(Generator\ClassGenerator $class, PHPClass $type)
{
$class = $this->fixInterfaces($class);

/**
* If the class doesn't already exist and there's a class with the same name as it's namespace
* (Example: the class is \garethp\ews\API\Type\EmailAddressType and the class \garethp\ews\API\Type exists)
* then we should extend that class.
*/
if (!($extends = $type->getExtends()) && class_exists($type->getNamespace())) {
$extendNamespace = $type->getNamespace();
$extendNamespace = explode('\\', $extendNamespace);
Expand Down Expand Up @@ -108,7 +118,15 @@ public function generate(Generator\ClassGenerator $class, PHPClass $type)
}
}

protected function handleBody(Generator\ClassGenerator $class, PHPClass $type)
/**
* This acts as our entrypoint to creating the body of the class, all the properties and methods that existing
* within it. We loop over the properties twice so that properties will always sit at the top of the class.
*
* @param Generator\ClassGenerator $class
* @param PHPClass $type
* @return bool
*/
protected function handleBody(Generator\ClassGenerator $class, PHPClass $type): bool
{
$this->handleEnumeration($class, $type);

Expand All @@ -117,6 +135,7 @@ protected function handleBody(Generator\ClassGenerator $class, PHPClass $type)
$this->handleProperty($class, $prop);
}
}

foreach ($type->getProperties() as $prop) {
if ($prop->getName() !== '__value') {
$this->handleMethod($class, $prop, $type);
Expand All @@ -130,7 +149,19 @@ protected function handleBody(Generator\ClassGenerator $class, PHPClass $type)
return true;
}

protected function handleProperty(Generator\ClassGenerator $class, PHPProperty $prop)
/**
* Here we generate the actual property for the class. We check if there's an existing property so that we can
* respect any Docblock changes that we've made ourselves. We attach the correct typing to the property since we
* don't have features such as Typed Arrays in PHP.
*
* We also check if the property is a type that we need to do some additional casting on, which is mostly DateTimes,
* Dates and Times. For these, we add them to a _typeMap property so that we can cast them when we need to.
*
* @param Generator\ClassGenerator $class
* @param PHPProperty $prop
* @return void
*/
protected function handleProperty(Generator\ClassGenerator $class, PHPProperty $prop): void
{
$generatedProp = new PropertyGenerator($prop->getName());
$generatedProp->setVisibility(PropertyGenerator::VISIBILITY_PROTECTED);
Expand Down Expand Up @@ -168,7 +199,16 @@ protected function handleProperty(Generator\ClassGenerator $class, PHPProperty $
}
}

protected function handleMethod(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class)
/**
* This acts as our entry point into creating the methods for our properties. For Arrays, we create an adder method,
* for boolean properties we create an is method and for all properties we create a getter and setter.
*
* @param Generator\ClassGenerator $generator
* @param PHPProperty $prop
* @param PHPClass $class
* @return void
*/
protected function handleMethod(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class): void
{
if ($prop->getType() instanceof PHPClassOf) {
$this->handleAdder($generator, $prop, $class);
Expand All @@ -182,7 +222,19 @@ protected function handleMethod(Generator\ClassGenerator $generator, PHPProperty
$this->handleSetter($generator, $prop, $class);
}

protected function handleAdder(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class)
/**
* add* methods are created for properties that are supposed to be collections, so we can add an item onto the
* array. However, the property can either be null for no values or a single value (due to how EWS returns
* collections), so we have a small check in the add* method to ensure that our property is an array before we add
* the value to it. If it's not an array, we convert it to an array (preserving any values in it) and then add the\
* item
*
* @param Generator\ClassGenerator $generator
* @param PHPProperty $prop
* @param PHPClass $class
* @return void
*/
protected function handleAdder(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class): void
{
$name = "add" . Inflector::classify($prop->getName());

Expand Down Expand Up @@ -232,7 +284,16 @@ protected function handleAdder(Generator\ClassGenerator $generator, PHPProperty
$generator->addMethodFromGenerator($generatedMethod);
}

protected function handleIs(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class)
/**
* is* methods are generated for boolean properties, it just returns the property cast as a boolean, which means
* that if the property is null it'll return false.
*
* @param Generator\ClassGenerator $generator
* @param PHPProperty $prop
* @param PHPClass $class
* @return void
*/
protected function handleIs(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class): void
{
$name = $prop->getName();
if (strtolower(substr($name, 0, 2)) !== "is") {
Expand Down Expand Up @@ -260,7 +321,20 @@ protected function handleIs(Generator\ClassGenerator $generator, PHPProperty $pr
$generator->addMethodFromGenerator($newMethod);
}

protected function handleGetter(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class)
/**
* This is where we generate our concrete getter method for the property. It's overall pretty simple, getter methods
* don't do much, however in a future version we'd like to force returning an array when we have a property that's
* meant to be a collection. EWS doesn't actually work like that, collections will sometimes come back as single
* items instead so at the moment properties that are meant to be collections can come back as single items. This
* will be a breaking change, so I've implemented the logic but left it disabled. Though it's probably going to be
* a better idea to handle this in the setter method instead.
*
* @param Generator\ClassGenerator $generator
* @param PHPProperty $prop
* @param PHPClass $class
* @return void
*/
protected function handleGetter(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class): void
{
$type = $this->getPropertyType($prop);
$namespace = explode("\\", $type);
Expand Down Expand Up @@ -304,7 +378,20 @@ protected function handleGetter(Generator\ClassGenerator $generator, PHPProperty
$generator->addMethodFromGenerator($newMethod);
}

protected function handleSetter(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class)
/**
* Here we generate the setter methods for each property. Type-mapped properties (Date, DateTime, Time) need to also
* accept string as a parameter since we'll be casting the value to the correct type. We also need to accept single
* values for properties that are meant to be collections. At the moment we're calling a castValueIfNeeded method
* for each property, however we want to inline this logic. In the future we probably also want to ensure
* collections get cast to arrays when setting them, rather than allowing single values from EWS. This is going to
* be a breaking change and require more testing.
*
* @param Generator\ClassGenerator $generator
* @param PHPProperty $prop
* @param PHPClass $class
* @return void
*/
protected function handleSetter(Generator\ClassGenerator $generator, PHPProperty $prop, PHPClass $class): void
{
$name = "set" . Inflector::classify($prop->getName());

Expand Down Expand Up @@ -360,7 +447,15 @@ protected function handleSetter(Generator\ClassGenerator $generator, PHPProperty
$generator->addMethodFromGenerator($newMethod);
}

protected function handleEnumeration(Generator\ClassGenerator $class, PHPClass $type)
/**
* If the type that we fetched from the xsd schema has an enumeration check on it's value, then we can create those
* enumerated constants on the class.
*
* @param Generator\ClassGenerator $class
* @param PHPClass $type
* @return void
*/
protected function handleEnumeration(Generator\ClassGenerator $class, PHPClass $type): void
{
if ($type->getChecks('__value') && isset($type->getChecks('__value')['enumeration'])) {
$enums = $type->getChecks('__value')['enumeration'];
Expand Down Expand Up @@ -392,10 +487,10 @@ protected function handleEnumeration(Generator\ClassGenerator $class, PHPClass $
protected function isOneType(PHPClass $type, $onlyParent = false)
{
if ($onlyParent) {
$e = $type->getExtends();
if ($e) {
if ($e->hasProperty('__value')) {
return $e->getProperty('__value');
$extension = $type->getExtends();
if ($extension) {
if ($extension->hasProperty('__value')) {
return $extension->getProperty('__value');
}
}
} else {
Expand All @@ -405,11 +500,17 @@ protected function isOneType(PHPClass $type, $onlyParent = false)
}
}

protected function getPhpType(PHPClass $class)
/**
* Gets the FQN of the PHP Type for the given class
*
* @param PHPClass $class
* @return string|null
*/
protected function getPhpType(PHPClass $class): ?string
{
if (!$class->getNamespace()) {
if ($this->isNativeType($class)) {
return $class->getName();
return (string) $class->getName();
}

return "\\" . $class->getName();
Expand All @@ -418,7 +519,13 @@ protected function getPhpType(PHPClass $class)
return "\\" . $class->getFullName();
}

protected function isNativeType(PHPClass $class)
/**
* Checks whether the given class is a PHP Native Type or not
*
* @param PHPClass $class
* @return bool
*/
protected function isNativeType(PHPClass $class): bool
{
return !$class->getNamespace() && in_array($class->getName(), [
'string',
Expand All @@ -432,7 +539,14 @@ protected function isNativeType(PHPClass $class)
]);
}

protected function isTypeMapped($class)
/**
* A simple check to see if the type needs to be cast when transforming from/to the XML. This is mostly for
* DateTimes, Dates and Times.
*
* @param $class
* @return bool
*/
protected function isTypeMapped($class): bool
{
$classMap = [
'dateTime',
Expand All @@ -443,6 +557,14 @@ protected function isTypeMapped($class)
return in_array($class, $classMap);
}

/**
* Checks whether we've auto-generated a method or not based on the presence of the @autogenerated tag in the
* DocBlock. This is useful for when we need to regenerate the class and don't want to overwrite any custom
* changes that have been made.
*
* @param Generator\MethodGenerator $method
* @return bool
*/
protected function isMethodAutoGenerated(Generator\MethodGenerator $method): bool
{
$tags = $method->getDocBlock()?->getTags() ?? [];
Expand All @@ -451,19 +573,32 @@ protected function isMethodAutoGenerated(Generator\MethodGenerator $method): boo
})) !== 0;
}

protected function getPropertyType($property)
/**
* Here, we fetch the PHP Type for a property. This won't be our concrete type-hint, since we'll return typed arrays
* such as "EmailAddress[]" however it'll return the information that we actually care about
*
* @param $property
* @return string
*/
protected function getPropertyType($property): string
{
$type = $property->getType();
$returnType = "";

// For some reason we were generating properties that were expecting a \garethp\ews\API\Type\LangAType for the
// xml property "lang" on ReplyBodyType without actually generating a "LangAType" class. Looking at EWS
// documentation it should just be the language code as a string, so we manually set it to be a string.
if ($property->getName() === "lang" && $type->getName() === "LangAType") {
return "string";
}

// PHPClassOf indicates that it's a collection of a single type. Even though we don't have typed arrays in PHP,
// we'll still return it as a typed array for Docblock purposes. When we create the actual concrete type-hints
// we'll detect it and turn it into array|SingleType.
if ($type && $type instanceof PHPClassOf) {
$tt = $type->getArg()->getType();
$returnType = $this->getPhpType($tt) . "[]";
if ($p = $this->isOneType($tt)) {
$singleType = $type->getArg()->getType();
$returnType = $this->getPhpType($singleType) . "[]";
if ($p = $this->isOneType($singleType)) {
if (($t = $p->getType())) {
$returnType = $this->getPhpType($t) . "[]";
}
Expand Down

0 comments on commit 245c1e8

Please sign in to comment.