-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ef312f3
commit 5b268e9
Showing
9 changed files
with
282 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper; | ||
|
||
/** | ||
* Attribute added to all "mapper" classes. | ||
* | ||
* Those classes should also implement MapperInterface. | ||
* | ||
* @author Ryan Weaver <ryan@symfonycasts.com> | ||
*/ | ||
#[\Attribute(\Attribute::TARGET_CLASS)] | ||
class AsMapper | ||
{ | ||
public function __construct( | ||
private string $from, | ||
private string $to, | ||
) | ||
{ | ||
} | ||
|
||
public function getFrom(): string | ||
{ | ||
return $this->from; | ||
} | ||
|
||
public function getTo(): string | ||
{ | ||
return $this->to; | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/Bundle/DependencyInjection/MicroMapperCompilerPass.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper\Bundle\DependencyInjection; | ||
|
||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; | ||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Definition; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
use Symfonycasts\MicroMapper\MapperConfig; | ||
|
||
/** | ||
* @author Ryan Weaver <ryan@symfonycasts.com> | ||
*/ | ||
class MicroMapperCompilerPass implements CompilerPassInterface | ||
{ | ||
public function process(ContainerBuilder $container) | ||
{ | ||
$mapperConfigDefinitions = []; | ||
foreach ($container->findTaggedServiceIds('micro_mapper.mapper') as $id => $tags) { | ||
foreach ($tags as $tag) { | ||
$mapperConfigDefinitions[] = new Definition(MapperConfig::class, [ | ||
$tag['from'], | ||
$tag['to'], | ||
new ServiceClosureArgument(new Reference($id)) | ||
]); | ||
} | ||
} | ||
$microMapperDefinition = $container->findDefinition('symfonycasts.micro_mapper'); | ||
$microMapperDefinition->setArgument(0, $mapperConfigDefinitions); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper\Bundle\DependencyInjection; | ||
|
||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; | ||
use Symfony\Component\Config\Definition\Builder\TreeBuilder; | ||
use Symfony\Component\Config\Definition\ConfigurationInterface; | ||
use Symfony\Component\Config\FileLocator; | ||
use Symfony\Component\DependencyInjection\ChildDefinition; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; | ||
use Symfony\Component\HttpKernel\DependencyInjection\Extension; | ||
use Symfonycasts\MicroMapper\AsMapper; | ||
|
||
/** | ||
* @author Ryan Weaver <ryan@symfonycasts.com> | ||
*/ | ||
class MicroMapperExtension extends Extension implements ConfigurationInterface | ||
{ | ||
public function load(array $configs, ContainerBuilder $container): void | ||
{ | ||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../config')); | ||
$loader->load('services.php'); | ||
|
||
$container->registerAttributeForAutoconfiguration(AsMapper::class, static function (ChildDefinition $definition, AsMapper $attribute) { | ||
$definition->addTag('micro_mapper.mapper', [ | ||
'from' => $attribute->getFrom(), | ||
'to' => $attribute->getTo(), | ||
]); | ||
}); | ||
} | ||
|
||
public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface | ||
{ | ||
return $this; | ||
} | ||
|
||
public function getAlias(): string | ||
{ | ||
return 'symfonycasts_micro_mapper'; | ||
} | ||
|
||
public function getConfigTreeBuilder(): TreeBuilder | ||
{ | ||
$treeBuilder = new TreeBuilder('symfonycasts_micro_mapper'); | ||
$rootNode = $treeBuilder->getRootNode(); | ||
\assert($rootNode instanceof ArrayNodeDefinition); | ||
|
||
// no configuration options yet | ||
|
||
return $treeBuilder; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; | ||
|
||
use Symfonycasts\MicroMapper\MicroMapper; | ||
use Symfonycasts\MicroMapper\MicroMapperInterface; | ||
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg; | ||
|
||
return static function (ContainerConfigurator $container): void { | ||
$container->services() | ||
->set('symfonycasts.micro_mapper', MicroMapper::class) | ||
->args([ | ||
abstract_arg('mapper configs array'), | ||
]) | ||
->alias(MicroMapperInterface::class, 'symfonycasts.micro_mapper') | ||
; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper; | ||
|
||
/** | ||
* Wrapper around an individual mapper. | ||
*/ | ||
class MapperConfig | ||
{ | ||
public function __construct( | ||
private string $fromClass, | ||
private string $toClass, | ||
private \Closure $mapper | ||
) | ||
{ | ||
} | ||
|
||
public function supports(object $fromObject, string $targetClass): bool | ||
{ | ||
return $fromObject instanceof $this->fromClass && $this->toClass === $targetClass; | ||
} | ||
|
||
public function getMapper(): MapperInterface | ||
{ | ||
return ($this->mapper)(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper; | ||
|
||
/** | ||
* Interface needed for each individual mapper. | ||
* | ||
* Also add #[AsMapper(from: Foo:class, to: Bar:class)] to each mapper class. | ||
* | ||
* @author Ryan Weaver <ryan@symfonycasts.com> | ||
*/ | ||
interface MapperInterface | ||
{ | ||
public function init(object $from, string $toClass, array $context): object; | ||
|
||
public function populate(object $from, object $to, array $context): object; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper; | ||
|
||
/** | ||
* @author Ryan Weaver <ryan@symfonycasts.com> | ||
*/ | ||
class MicroMapper implements MicroMapperInterface | ||
{ | ||
private array $objectHashes = []; | ||
|
||
private int $currentDepth = 0; | ||
private ?int $maxDepth = null; | ||
|
||
/** | ||
* @param MapperConfig[] $mapperConfigs | ||
*/ | ||
public function __construct(private array $mapperConfigs) | ||
{ | ||
} | ||
|
||
public function map(object $from, string $toClass, array $context = []): object | ||
{ | ||
$this->currentDepth++; | ||
|
||
if ($this->currentDepth > 50) { | ||
throw new \Exception('Max depth reached'); | ||
} | ||
|
||
// set the max depth if not already set | ||
// the max depth is recorded as MAX_DEPTH + the current depth | ||
$previousMaxDepth = $this->maxDepth; | ||
if (isset($context[self::MAX_DEPTH]) | ||
&& (null === $this->maxDepth || ($context[self::MAX_DEPTH] + $this->currentDepth) < $this->maxDepth) | ||
) { | ||
$this->maxDepth = $context[self::MAX_DEPTH] + $this->currentDepth; | ||
} | ||
|
||
$shouldFullyPopulate = $this->maxDepth === null || $this->currentDepth < $this->maxDepth; | ||
|
||
// watch for circular references, but only if we're fully populating | ||
// if we are not fully populating, this is already the final depth/level | ||
// through the micro mapper. | ||
if (isset($this->objectHashes[spl_object_hash($from)]) && $shouldFullyPopulate) { | ||
throw new \Exception(sprintf( | ||
'Circular reference detected with micro mapper: %s. Try passing [MicroMapperInterface::MAX_DEPTH => 1] when mapping relationships.', | ||
implode(' -> ', array_merge($this->objectHashes, [get_class($from)])) | ||
)); | ||
} | ||
|
||
$this->objectHashes[spl_object_hash($from)] = get_class($from); | ||
|
||
foreach ($this->mapperConfigs as $mapperConfig) { | ||
if (!$mapperConfig->supports($from, $toClass)) { | ||
continue; | ||
} | ||
|
||
$toObject = $mapperConfig->getMapper()->init($from, $toClass, $context); | ||
|
||
// avoid fully populated objects if max depth is reached | ||
if ($this->maxDepth === null || $this->currentDepth < $this->maxDepth) { | ||
$mapperConfig->getMapper()->populate($from, $toObject, $context); | ||
} | ||
|
||
unset($this->objectHashes[spl_object_hash($from)]); | ||
$this->currentDepth--; | ||
$this->maxDepth = $previousMaxDepth; | ||
|
||
return $toObject; | ||
} | ||
|
||
throw new \Exception(sprintf('No mapper found for %s -> %s', get_class($from), $toClass)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper; | ||
|
||
/** | ||
* Maps one object to another using the configured mappers. | ||
*/ | ||
interface MicroMapperInterface | ||
{ | ||
public const MAX_DEPTH = 'max_depth'; | ||
|
||
public function map(object $from, string $toClass, array $context = []): object; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
namespace Symfonycasts\MicroMapper; | ||
|
||
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; | ||
use Symfony\Component\HttpKernel\Bundle\Bundle; | ||
use Symfonycasts\MicroMapper\Bundle\DependencyInjection\MicroMapperExtension; | ||
|
||
/** | ||
* @author Ryan Weaver <ryan@symfonycasts.com> | ||
*/ | ||
class SymfonycastsMicroMapperBundle extends Bundle | ||
{ | ||
protected function createContainerExtension(): ?ExtensionInterface | ||
{ | ||
return new MicroMapperExtension(); | ||
} | ||
} |