Skip to content

Commit

Permalink
Merge pull request #14 from veewee/precalculated-object-access
Browse files Browse the repository at this point in the history
Pre-calculate object access tools
  • Loading branch information
veewee authored Jun 14, 2024
2 parents 2ac0cfc + 1bd7520 commit f2f7de2
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 97 deletions.
98 changes: 98 additions & 0 deletions src/Encoder/ObjectAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);

namespace Soap\Encoding\Encoder;

use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\TypeMeta;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\Lens;
use function Psl\Vec\sort_by;
use function VeeWee\Reflecta\Lens\index;
use function VeeWee\Reflecta\Lens\optional;
use function VeeWee\Reflecta\Lens\property;

final class ObjectAccess
{
/**
* @param array<string, Property> $properties
* @param array<string, Lens<object, mixed>> $encoderLenses
* @param array<string, Lens<array, mixed>> $decoderLenses
* @param array<string, Iso<mixed, string>> $isos
*/
public function __construct(
public readonly array $properties,
public readonly array $encoderLenses,
public readonly array $decoderLenses,
public readonly array $isos,
public readonly bool $isAnyPropertyQualified
) {
}

public static function forContext(Context $context): self
{
$type = ComplexTypeBuilder::default()($context);

$sortedProperties = sort_by(
$type->getProperties(),
static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false),
);

$normalizedProperties = [];
$encoderLenses = [];
$decoderLenses = [];
$isos = [];
$isAnyPropertyQualified = false;

foreach ($sortedProperties as $property) {
$typeMeta = $property->getType()->getMeta();
$name = $property->getName();
$normalizedName = PhpPropertyNameNormalizer::normalize($name);

$shouldLensBeOptional = self::shouldLensBeOptional($typeMeta);
$normalizedProperties[$normalizedName] = $property;
$encoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(property($normalizedName)) : property($normalizedName);
$decoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(index($name)) : index($name);
$isos[$normalizedName] = self::grabIsoForProperty($context, $property);

$isAnyPropertyQualified = $isAnyPropertyQualified || $typeMeta->isQualified()->unwrapOr(false);
}

return new self(
$normalizedProperties,
$encoderLenses,
$decoderLenses,
$isos,
$isAnyPropertyQualified
);
}

private static function shouldLensBeOptional(TypeMeta $meta): bool
{
if ($meta->isNullable()->unwrapOr(false)) {
return true;
}

if (
$meta->isAttribute()->unwrapOr(false) &&
$meta->use()->unwrapOr('optional') === 'optional'
) {
return true;
}

return false;
}

/**
* @return Iso<mixed, string>
*/
private static function grabIsoForProperty(Context $context, Property $property): Iso
{
$propertyContext = $context->withType($property->getType());

return $context->registry->detectEncoderForContext($propertyContext)
->iso($propertyContext);
}
}
124 changes: 27 additions & 97 deletions src/Encoder/ObjectEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

use Closure;
use Exception;
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
use Soap\Encoding\TypeInference\XsiTypeDetector;
use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
Expand All @@ -15,19 +13,11 @@
use Soap\Encoding\Xml\Writer\XsdTypeXmlElementWriter;
use Soap\Encoding\Xml\Writer\XsiAttributeBuilder;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\TypeMeta;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\Lens;
use function is_array;
use function Psl\Dict\map;
use function Psl\Dict\map_with_key;
use function Psl\Dict\reindex;
use function Psl\Iter\any;
use function Psl\Vec\sort_by;
use function VeeWee\Reflecta\Iso\object_data;
use function VeeWee\Reflecta\Lens\index;
use function VeeWee\Reflecta\Lens\optional;
use function VeeWee\Reflecta\Lens\property;
use function VeeWee\Xml\Writer\Builder\children as writeChildren;
use function VeeWee\Xml\Writer\Builder\raw;
use function VeeWee\Xml\Writer\Builder\value as buildValue;
Expand All @@ -52,24 +42,24 @@ public function __construct(
*/
public function iso(Context $context): Iso
{
$properties = $this->detectProperties($context);
$objectAccess = ObjectAccess::forContext($context);

return new Iso(
/**
* @param TObj|array $value
* @return non-empty-string
*/
function (object|array $value) use ($context, $properties) : string {
return $this->to($context, $properties, $value);
function (object|array $value) use ($context, $objectAccess) : string {
return $this->to($context, $objectAccess, $value);
},
/**
* @param non-empty-string|Element $value
* @return TObj
*/
function (string|Element $value) use ($context, $properties) : object {
function (string|Element $value) use ($context, $objectAccess) : object {
return $this->from(
$context,
$properties,
$objectAccess,
($value instanceof Element ? $value : Element::fromString($value))
);
}
Expand All @@ -78,19 +68,14 @@ function (string|Element $value) use ($context, $properties) : object {

/**
* @param TObj|array $data
* @param array<string, Property> $properties
*
* @return non-empty-string
*/
private function to(Context $context, array $properties, object|array $data): string
private function to(Context $context, ObjectAccess $objectAccess, object|array $data): string
{
if (is_array($data)) {
$data = (object) $data;
}
$isAnyPropertyQualified = any(
$properties,
static fn (Property $property): bool => $property->getType()->getMeta()->isQualified()->unwrapOr(false)
);
$defaultAction = writeChildren([]);

return (new XsdTypeXmlElementWriter())(
Expand All @@ -100,32 +85,31 @@ private function to(Context $context, array $properties, object|array $data): st
(new XsiAttributeBuilder(
$context,
XsiTypeDetector::detectFromValue($context, []),
includeXsiTargetNamespace: !$isAnyPropertyQualified,
includeXsiTargetNamespace: !$objectAccess->isAnyPropertyQualified,
)),
...map_with_key(
$properties,
function (string $normalizePropertyName, Property $property) use ($context, $data, $defaultAction) : Closure {
$objectAccess->properties,
static function (string $normalizePropertyName, Property $property) use ($objectAccess, $data, $defaultAction) : Closure {
$type = $property->getType();
$meta = $type->getMeta();
$isAttribute = $meta->isAttribute()->unwrapOr(false);

/** @var mixed $value */
$value = $this->runLens(
property($normalizePropertyName),
$meta,
$data,
null
$value = self::runLens(
$objectAccess->encoderLenses[$normalizePropertyName],
$data
);
$iso = $objectAccess->isos[$normalizePropertyName];

return match(true) {
$isAttribute => $value ? (new AttributeBuilder(
$type,
$this->grabIsoForProperty($context, $property)->to($value)
$iso->to($value)
))(...) : $defaultAction,
$property->getName() === '_' => $value
? buildValue($this->grabIsoForProperty($context, $property)->to($value))
? buildValue($iso->to($value))
: (new NilAttributeBuilder())(...),
default => $value ? raw($this->grabIsoForProperty($context, $property)->to($value)) : $defaultAction
default => $value ? raw($iso->to($value)) : $defaultAction
};
}
)
Expand All @@ -135,100 +119,46 @@ function (string $normalizePropertyName, Property $property) use ($context, $dat
}

/**
* @param array<string, Property> $properties
*
* @return TObj
*/
private function from(Context $context, array $properties, Element $data): object
private function from(Context $context, ObjectAccess $objectAccess, Element $data): object
{
$nodes = (new DocumentToLookupArrayReader())($data);
/** @var Iso<TObj, array<string, mixed>> $objectData */
$objectData = object_data($this->className);

return $objectData->from(
map(
$properties,
function (Property $property) use ($context, $nodes): mixed {
map_with_key(
$objectAccess->properties,
static function (string $normalizePropertyName, Property $property) use ($objectAccess, $nodes): mixed {
$type = $property->getType();
$meta = $type->getMeta();

/** @var string|null $value */
$value = $this->runLens(
index($property->getName()),
$meta,
$value = self::runLens(
$objectAccess->decoderLenses[$normalizePropertyName],
$nodes,
null
);
$iso = $objectAccess->isos[$normalizePropertyName];
$defaultValue = $meta->isList()->unwrapOr(false) ? [] : null;

/** @psalm-suppress PossiblyNullArgument */
return match(true) {
$meta->isAttribute()->unwrapOr(false) => $this->grabIsoForProperty($context, $property)->from($value),
default => $value !== null ? $this->grabIsoForProperty($context, $property)->from($value) : $defaultValue,
$meta->isAttribute()->unwrapOr(false) => $iso->from($value),
default => $value !== null ? $iso->from($value) : $defaultValue,
};
},
)
);
}

/**
* @return Iso<mixed, string>
*/
private function grabIsoForProperty(Context $context, Property $property): Iso
{
$propertyContext = $context->withType($property->getType());

return $context->registry->detectEncoderForContext($propertyContext)
->iso($propertyContext);
}

private function runLens(Lens $lens, TypeMeta $meta, mixed $data, mixed $default): mixed
private static function runLens(Lens $lens, mixed $data, mixed $default = null): mixed
{
try {
/** @var mixed */
return $this->decorateLensForType($lens, $meta)->get($data);
return $lens->get($data);
} catch (Exception $e) {
return $default;
}
}

/**
* @template S
* @template A
*
* @param Lens<S, A> $lens
*
* @return Lens<S, A>
*/
private function decorateLensForType(Lens $lens, TypeMeta $meta): Lens
{
if ($meta->isNullable()->unwrapOr(false)) {
return optional($lens);
}

if (
$meta->isAttribute()->unwrapOr(false) &&
$meta->use()->unwrapOr('optional') === 'optional'
) {
return optional($lens);
}

return $lens;
}

/**
* @return array<string, Property>
*/
private function detectProperties(Context $context): array
{
$type = ComplexTypeBuilder::default()($context);

return reindex(
sort_by(
$type->getProperties(),
static fn (Property $property): bool => !$property->getType()->getMeta()->isAttribute()->unwrapOr(false),
),
static fn (Property $property): string => PhpPropertyNameNormalizer::normalize($property->getName()),
);
}
}
2 changes: 2 additions & 0 deletions tests/Unit/Encoder/ObjectEncoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use PHPUnit\Framework\Attributes\CoversClass;
use Soap\Encoding\Encoder\Context;
use Soap\Encoding\Encoder\ObjectAccess;
use Soap\Encoding\Encoder\ObjectEncoder;
use Soap\Encoding\Test\Fixture\Model\Hat;
use Soap\Encoding\Test\Fixture\Model\User;
Expand All @@ -21,6 +22,7 @@
use function Psl\Fun\tap;

#[CoversClass(ObjectEncoder::class)]
#[CoversClass(ObjectAccess::class)]
final class ObjectEncoderTest extends AbstractEncoderTests
{
public static function provideIsomorphicCases(): iterable
Expand Down

0 comments on commit f2f7de2

Please sign in to comment.