Skip to content

Commit

Permalink
Merge pull request #103 from jolicode/feat/debug-mode
Browse files Browse the repository at this point in the history
feat(debug): add debug command and profiler for symfony
  • Loading branch information
joelwurtz committed Apr 2, 2024
2 parents 0fbe6af + e61cea7 commit 80336ef
Show file tree
Hide file tree
Showing 37 changed files with 614 additions and 30 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"moneyphp/money": "^3.3.2",
"phpdocumentor/type-resolver": "^1.7",
"phpunit/phpunit": "^9.0",
"symfony/browser-kit": "^7.0",
"symfony/browser-kit": "^6.4 || ^7.0",
"symfony/console": "^6.4 || ^7.0",
"symfony/filesystem": "^6.4 || ^7.0",
"symfony/framework-bundle": "*",
"symfony/http-client": "^6.4 || ^7.0",
Expand Down
2 changes: 2 additions & 0 deletions docs/_nav.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
- [Configuration](bundle/configuration.md)
- [Cache Warmup](bundle/cache-warmup.md)
- [Expression Language](bundle/expression-language.md)
- [Api Platform](bundle/api-platform.md)
- [Migrate existing application](bundle/migrate.md)
- [Debugging](bundle/debugging.md)
- [Upgrading to 9.0](upgrading-9.0.md)
- [Contributing](contributing.md)
25 changes: 25 additions & 0 deletions docs/bundle/debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Debug a Mapper

AutoMapper provides 2 ways to debug what's going on with a mapper when using the Symfony bundle:

## The `debug:mapper` Command

The `debug:mapper` command will display the mapping information for a specific mapper.
This can be useful to understand how AutoMapper is mapping your objects and why some properties are not mapped.

```bash
php bin/console debug:mapper User UserDTO
```

![Profiler](../images/debug-cli.png)

## Using the symfony profiler

AutoMapper provides a panel in the Symfony profiler that will display the mapping information for each request.
Please note that this only display Mapper that has been generated during the request, if you have a mapper that was not
generated during the request it will not be displayed.

You can find the panel in the Symfony profiler under the `AutoMapper` tab.

![Profiler](../images/debug-profiler-1.png)
![Profiler](../images/debug-profiler-2.png)
2 changes: 2 additions & 0 deletions docs/bundle/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ features linked to Symfony way of doing things.
- [Configuration](configuration.md)
- [Cache Warmup](cache-warmup.md)
- [Expression Language](expression-language.md)
- [Api Platform](api-platform.md)
- [Migrate existing application](migrate.md)
- [Debugging](debugging.md)
Binary file added docs/images/debug-cli.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/debug-profiler-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/debug-profiler-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function __construct(
public ?int $maxDepth = null,
public ?TransformerInterface $transformer = null,
public ?bool $ignored = null,
public ?string $ignoreReason = null,
public ?string $if = null,
public ?array $groups = null,
public ?bool $disableGroupsCheck = null,
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/MapFromListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
maxDepth: $mapFrom->maxDepth,
transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->target, $mapFrom, false),
ignored: $mapFrom->ignore,
ignoreReason: $mapFrom->ignore === true ? 'Property is ignored by MapFrom Attribute on Target' : null,
if: $mapFrom->if,
groups: $mapFrom->groups,
);
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/MapToListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
maxDepth: $mapTo->maxDepth,
transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->source, $mapTo),
ignored: $mapTo->ignore,
ignoreReason: $mapTo->ignore === true ? 'Property is ignored by MapTo Attribute on Source' : null,
if: $mapTo->if,
groups: $mapTo->groups,
);
Expand Down
2 changes: 2 additions & 0 deletions src/EventListener/Symfony/SerializerIgnoreListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ public function __invoke(PropertyMetadataEvent $event): void

if (($event->mapperMetadata->source !== 'array' && $event->mapperMetadata->source !== \stdClass::class) && $this->isIgnoreProperty($event->mapperMetadata->source, $event->source->name)) {
$event->ignored = true;
$event->ignoreReason = 'Property is ignored by Symfony Serializer Attribute on Source';

return;
}

if (($event->mapperMetadata->target !== 'array' && $event->mapperMetadata->target !== \stdClass::class) && $this->isIgnoreProperty($event->mapperMetadata->target, $event->target->name)) {
$event->ignored = true;
$event->ignoreReason = 'Property is ignored by Symfony Serializer Attribute on Target';
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Metadata/MapperMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class MapperMetadata
public function __construct(
public string $source,
public string $target,
public bool $registered,
private string $classPrefix = 'Mapper_',
) {
if (class_exists($this->source) && !\in_array($this->source, ['array', \stdClass::class], true)) {
Expand Down
30 changes: 28 additions & 2 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use AutoMapper\Extractor\ReadWriteTypeExtractor;
use AutoMapper\Extractor\SourceTargetMappingExtractor;
use AutoMapper\Extractor\WriteMutator;
use AutoMapper\Transformer\AllowNullValueTransformerInterface;
use AutoMapper\Transformer\ArrayTransformerFactory;
use AutoMapper\Transformer\BuiltinTransformerFactory;
use AutoMapper\Transformer\ChainTransformerFactory;
Expand All @@ -37,6 +38,7 @@
use AutoMapper\Transformer\SymfonyUidTransformerFactory;
use AutoMapper\Transformer\TransformerFactoryInterface;
use AutoMapper\Transformer\UniqueTypeTransformerFactory;
use AutoMapper\Transformer\VoidTransformer;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
Expand Down Expand Up @@ -118,6 +120,18 @@ public function resolveAllMetadata(MetadataRegistry $metadataRegistry): void
}
}

/**
* @return iterable<GeneratorMetadata>
*/
public function listMetadata(): iterable
{
foreach ($this->generatorMetadata as $targets) {
foreach ($targets as $metadata) {
yield $metadata;
}
}
}

private function createGeneratorMetadata(MapperMetadata $mapperMetadata): GeneratorMetadata
{
$extractor = $this->sourceTargetPropertiesMappingExtractor;
Expand Down Expand Up @@ -225,18 +239,30 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
$transformer = $this->transformerFactory->getTransformer($propertyMappedEvent->types, $sourcePropertyMetadata, $targetPropertyMetadata, $mapperMetadata);

if (null === $transformer) {
continue;
$propertyMappedEvent->ignored = true;
$propertyMappedEvent->ignoreReason = 'We didn\'t find a way to correctly transform this property.';
}

$propertyMappedEvent->transformer = $transformer;
}

if ($sourcePropertyMetadata->accessor === null && !($propertyMappedEvent->transformer instanceof AllowNullValueTransformerInterface)) {
$propertyMappedEvent->ignored = true;
$propertyMappedEvent->ignoreReason = 'Property cannot be read from source, and the attached transformer require a value.';
}

if ($targetPropertyMetadata->writeMutator === null && $targetPropertyMetadata->parameterInConstructor === null) {
$propertyMappedEvent->ignored = true;
$propertyMappedEvent->ignoreReason = 'Property cannot be write on target.';
}

$propertiesMapping[] = new PropertyMetadata(
$sourcePropertyMetadata,
$targetPropertyMetadata,
$propertyMappedEvent->types,
$propertyMappedEvent->transformer,
$propertyMappedEvent->transformer ?? new VoidTransformer(),
$propertyMappedEvent->ignored ?? false,
$propertyMappedEvent->ignoreReason ?? '',
$propertyMappedEvent->maxDepth,
$propertyMappedEvent->if,
$propertyMappedEvent->groups,
Expand Down
18 changes: 13 additions & 5 deletions src/Metadata/MetadataRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ public function __construct(
* @param class-string<object>|'array' $source
* @param class-string<object>|'array' $target
*/
public function get(string $source, string $target): MapperMetadata
public function get(string $source, string $target, bool $registered = false): MapperMetadata
{
$source = $this->getRealClassName($source);
$target = $this->getRealClassName($target);

return $this->registry[$source][$target] ??= new MapperMetadata($source, $target, $this->configuration->classPrefix);
return $this->registry[$source][$target] ??= new MapperMetadata($source, $target, $registered, $this->configuration->classPrefix);
}

/**
Expand All @@ -43,19 +43,27 @@ public function get(string $source, string $target): MapperMetadata
*/
public function register(string $source, string $target): void
{
$this->get($source, $target);
$this->get($source, $target, true);
}

/**
* @param class-string<object>|'array' $source
* @param class-string<object>|'array' $target
*/
public function has(string $source, string $target): bool
public function has(string $source, string $target, bool $onlyRegistered): bool
{
$source = $this->getRealClassName($source);
$target = $this->getRealClassName($target);

return isset($this->registry[$source][$target]);
if (!isset($this->registry[$source][$target])) {
return false;
}

if ($onlyRegistered) {
return $this->registry[$source][$target]->registered;
}

return true;
}

public function getIterator(): \Traversable
Expand Down
1 change: 1 addition & 0 deletions src/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public function __construct(
public readonly TypesMatching $types,
public TransformerInterface $transformer,
public bool $ignored = false,
public string $ignoreReason = '',
public ?int $maxDepth = null,
public ?string $if = null,
public ?array $groups = null,
Expand Down
8 changes: 4 additions & 4 deletions src/Normalizer/AutoMapperNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public function supportsNormalization(mixed $data, string $format = null, array
return true;
}

return $this->onlyMetadataRegistry->has($data::class, 'array');
return $this->onlyMetadataRegistry->has($data::class, 'array', true);
}

/**
Expand All @@ -112,7 +112,7 @@ public function supportsDenormalization(mixed $data, string $type, string $forma
return true;
}

return $this->onlyMetadataRegistry->has('array', $type);
return $this->onlyMetadataRegistry->has('array', $type, true);
}

public function getSupportedTypes(?string $format): array
Expand All @@ -125,12 +125,12 @@ public function getSupportedTypes(?string $format): array

foreach ($this->onlyMetadataRegistry as $metadata) {
if ($metadata->source === 'array') {
$hasTarget = $this->onlyMetadataRegistry->has($metadata->target, 'array');
$hasTarget = $this->onlyMetadataRegistry->has($metadata->target, 'array', true);

// Only cache when both source and target exist in the registry
$types[$metadata->target] = $hasTarget;
} elseif ($metadata->target === 'array') {
$hasSource = $this->onlyMetadataRegistry->has($metadata->target, 'array');
$hasSource = $this->onlyMetadataRegistry->has($metadata->target, 'array', true);

// Only cache when both source and target exist in the registry
$types[$metadata->source] = $hasSource;
Expand Down
103 changes: 103 additions & 0 deletions src/Symfony/Bundle/Command/DebugMapperCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Symfony\Bundle\Command;

use AutoMapper\Metadata\Dependency;
use AutoMapper\Metadata\MetadataFactory;
use AutoMapper\Metadata\PropertyMetadata;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

final class DebugMapperCommand extends Command
{
public function __construct(
private readonly MetadataFactory $metadataFactory
) {
parent::__construct('debug:mapper');
}

protected function configure(): void
{
$this
->setDescription('Debug Mapper from source to target')
->addArgument('source', InputArgument::REQUIRED, 'Source class or "array"')
->addArgument('target', InputArgument::REQUIRED, 'Target class or "array"')
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var class-string<object>|'array' $source */
$source = $input->getArgument('source');
/** @var class-string<object>|'array' $target */
$target = $input->getArgument('target');

$metadata = $this->metadataFactory->getGeneratorMetadata($source, $target);

$style = new SymfonyStyle($input, $output);
$style->section('Mapper:');

$style->horizontalTable(
['Source', 'Target', 'Classname', 'Check attributes', 'Use constructor', 'Provider'],
[
[
$metadata->mapperMetadata->source,
$metadata->mapperMetadata->target,
$metadata->mapperMetadata->className,
$metadata->checkAttributes ? 'Yes' : 'No',
$metadata->hasConstructor() ? 'Yes' : 'No',
$metadata->provider,
],
]);

$style->section('Dependencies:');

$style->table(
['Mapper', 'Source', 'Target'],
array_map(
fn (Dependency $dependency) => [
$dependency->mapperDependency->name,
$dependency->mapperDependency->source,
$dependency->mapperDependency->target,
],
$metadata->getDependencies()
)
);

$style->section('Used Properties:');

$style->table(
[sprintf('%s -> %s', $source, $target), 'If', 'Transformer', 'Groups', 'MaxDepth'],
array_map(
fn (PropertyMetadata $property) => [
$property->source->name . ' -> ' . $property->target->name,
$property->if,
\get_class($property->transformer),
$property->disableGroupsCheck ? 'Disabled' : implode(', ', $property->groups ?? []),
$property->maxDepth,
],
array_filter($metadata->propertiesMetadata, fn (PropertyMetadata $property) => !$property->ignored)
)
);

$style->section('Not Used Properties:');

$style->table(
[sprintf('%s -> %s', $source, $target), 'Not used reason'],
array_map(
fn (PropertyMetadata $property) => [
$property->source->name . ' -> ' . $property->target->name,
$property->ignoreReason,
],
array_filter($metadata->propertiesMetadata, fn (PropertyMetadata $property) => $property->ignored)
)
);

return Command::SUCCESS;
}
}
Loading

0 comments on commit 80336ef

Please sign in to comment.