Skip to content

Commit

Permalink
[Map] Complete and simplify the normalization/denormalization process…
Browse files Browse the repository at this point in the history
… of Map's value objects, add MapOptionsNormalizer
  • Loading branch information
Kocal committed Nov 17, 2024
1 parent 66b1bcf commit 1fe568b
Show file tree
Hide file tree
Showing 14 changed files with 425 additions and 47 deletions.
18 changes: 9 additions & 9 deletions src/Map/src/Bridge/Google/tests/GoogleRendererTest.php

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Marker;
use Symfony\UX\Map\Point;
use Symfony\UX\Map\Polygon;
use Symfony\UX\Map\Test\RendererTestCase;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;

Expand All @@ -28,24 +29,32 @@ public function provideTestRenderMap(): iterable
->zoom(12);

yield 'simple map' => [
'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-center-value="&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;" data-symfony--ux-leaflet-map--map-markers-value="&#x5B;&#x5D;" data-symfony--ux-leaflet-map--map-polygons-value="&#x5B;&#x5D;"></div>',
'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]"></div>',
'renderer' => new LeafletRenderer(new StimulusHelper(null)),
'map' => $map,
];

yield 'with custom attributes' => [
'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-center-value="&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;" data-symfony--ux-leaflet-map--map-markers-value="&#x5B;&#x5D;" data-symfony--ux-leaflet-map--map-polygons-value="&#x5B;&#x5D;" class="map"></div>',
'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" class="map"></div>',
'renderer' => new LeafletRenderer(new StimulusHelper(null)),
'map' => $map,
'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
];

yield 'with markers and infoWindows' => [
'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-center-value="&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;" data-symfony--ux-leaflet-map--map-markers-value="&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;" data-symfony--ux-leaflet-map--map-polygons-value="&#x5B;&#x5D;"></div>',
'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Paris&quot;,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;abd45a2c703af97a&quot;},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;219637cbd8e62ea6&quot;}]" data-symfony--ux-leaflet-map--map-polygons-value="[]"></div>',
'renderer' => new LeafletRenderer(new StimulusHelper(null)),
'map' => (clone $map)
->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
->addMarker(new Marker(new Point(48.8566, 2.3522), 'Lyon', infoWindow: new InfoWindow(content: 'Lyon'))),
];

yield 'with polygons and infoWindows' => [
'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]"></div>',
'renderer' => new LeafletRenderer(new StimulusHelper(null)),
'map' => (clone $map)
->addPolygon(new Polygon(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)]))
->addPolygon(new Polygon(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))),
];
}
}
34 changes: 34 additions & 0 deletions src/Map/src/Exception/UnableToDenormalizeOptionsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Map\Exception;

use Symfony\UX\Map\MapOptionsInterface;

final class UnableToDenormalizeOptionsException extends LogicException
{
public function __construct(string $message)
{
parent::__construct(\sprintf('Unable to denormalize the map options: %s', $message));
}

public static function missingProviderKey(string $key): self
{
return new self(\sprintf('the provider key ("%s") is missing in the normalized options.', $key));
}

public static function unsupportedProvider(string $provider, array $supportedProviders): self
{
return new self(\sprintf('the provider "%s" is not supported. Supported providers are "%s".', $provider, implode('", "', $supportedProviders)));
}
}
32 changes: 32 additions & 0 deletions src/Map/src/Exception/UnableToNormalizeOptionsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Map\Exception;

use Symfony\UX\Map\MapOptionsInterface;

final class UnableToNormalizeOptionsException extends LogicException
{
public function __construct(string $message)
{
parent::__construct(\sprintf('Unable to normalize the map options: %s', $message));
}

/**
* @param class-string<MapOptionsInterface> $classOptions
*/
public static function unsupportedProviderClass(string $classOptions): self
{
return new self(\sprintf('the class "%s" is not supported.', $classOptions));
}
}
8 changes: 2 additions & 6 deletions src/Map/src/InfoWindow.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function toArray(): array
'position' => $this->position?->toArray(),
'opened' => $this->opened,
'autoClose' => $this->autoClose,
'extra' => (object) $this->extra,
'extra' => $this->extra,
];
}

Expand All @@ -61,7 +61,7 @@ public function toArray(): array
* position: array{lat: float, lng: float}|null,
* opened: bool,
* autoClose: bool,
* extra: object,
* extra: array,
* } $data
*
* @internal
Expand All @@ -71,10 +71,6 @@ public static function fromArray(array $data): self
if (isset($data['position'])) {
$data['position'] = Point::fromArray($data['position']);
}

if (isset($data['extra'])) {
$data['extra'] = (array) $data['extra'];
}

return new self(...$data);
}
Expand Down
75 changes: 75 additions & 0 deletions src/Map/src/Live/ComponentWithMapTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Map\Live;

use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\Map\Map;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\Attribute\PostMount;

/**
*
* @author Hugo Alliaume <[email protected]>
*/
trait ComponentWithMapTrait
{
/**
* @internal
*/
#[LiveProp(hydrateWith: 'hydrateMap', dehydrateWith: 'dehydrateMap')]
#[ExposeInTemplate(getter: 'getMap')]
public ?Map $map = null;

abstract protected function instantiateMap(): Map;

public function getMap(): Map
{
if (null === $this->map) {
$this->map = $this->instantiateMap();
}

return $this->map;
}

/**
* @internal
*/
#[PostMount]
public function initializeMap(array $data): array
{
// allow the Map object to be passed into the component() as "map"
if (\array_key_exists('map', $data)) {
$this->map = $data['map'];
unset($data['map']);
}

return $data;
}

/**
* @internal
*/
public function hydrateMap(array $data): Map
{
return Map::fromArray($data);
}

/**
* @internal
*/
public function dehydrateMap(Map $map): array
{
return $map->toArray();
}
}
8 changes: 6 additions & 2 deletions src/Map/src/Map.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public function toArray(): array
'center' => $this->center?->toArray(),
'zoom' => $this->zoom,
'fitBoundsToMarkers' => $this->fitBoundsToMarkers,
'options' => (object) ($this->options?->toArray() ?? []),
'options' => $this->options ? MapOptionsNormalizer::normalize($this->options) : [],
'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers),
'polygons' => array_map(static fn (Polygon $polygon) => $polygon->toArray(), $this->polygons),
];
Expand All @@ -124,7 +124,7 @@ public function toArray(): array
* markers?: list<array>,
* polygons?: list<array>,
* fitBoundsToMarkers?: bool,
* options?: object,
* options?: array<string, mixed>,
* } $map
*
* @internal
Expand All @@ -133,6 +133,10 @@ public static function fromArray(array $map): self
{
$map['fitBoundsToMarkers'] = true;

if (isset($map['options'])) {
$map['options'] = [] === $map['options'] ? null : MapOptionsNormalizer::denormalize($map['options']);
}

if (isset($map['center'])) {
$map['center'] = Point::fromArray($map['center']);
}
Expand Down
67 changes: 67 additions & 0 deletions src/Map/src/MapOptionsNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Map;

use Symfony\UX\Map\Bridge as MapBridge;
use Symfony\UX\Map\Exception\UnableToDenormalizeOptionsException;
use Symfony\UX\Map\Exception\UnableToNormalizeOptionsException;

/**
* Normalizes and denormalizes map options.
*
* @internal
* @author Hugo Alliaume <[email protected]>
*/
final class MapOptionsNormalizer
{
private const string KEY_PROVIDER = '@provider';

/**
* @var array<string, class-string<MapOptionsInterface>>
*/
public static array $providers = [
'google' => MapBridge\Google\GoogleOptions::class,
'leaflet' => MapBridge\Leaflet\LeafletOptions::class,
];

public static function denormalize(array $array): MapOptionsInterface
{
if (null === ($provider = $array[self::KEY_PROVIDER] ?? null)) {
throw UnableToDenormalizeOptionsException::missingProviderKey(self::KEY_PROVIDER);
}

if (!isset(self::$providers[$provider])) {
throw UnableToDenormalizeOptionsException::unsupportedProvider($provider, array_keys(self::$providers));
}

unset($array[self::KEY_PROVIDER]);

$class = self::$providers[$provider];

return $class::fromArray($array);
}

public static function normalize(MapOptionsInterface $options): array
{
$provider = array_search($options::class, self::$providers, true);
if (!\is_string($provider)) {
throw UnableToNormalizeOptionsException::unsupportedProviderClass($options::class);
}

$array = $options->toArray();
$array[self::KEY_PROVIDER] = $provider;

return $array;
}
}
2 changes: 1 addition & 1 deletion src/Map/src/Marker.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function toArray(): array
'position' => $this->position->toArray(),
'title' => $this->title,
'infoWindow' => $this->infoWindow?->toArray(),
'extra' => (object) $this->extra,
'extra' => $this->extra,
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/Map/src/Polygon.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function toArray(): array
'points' => array_map(fn (Point $point) => $point->toArray(), $this->points),
'title' => $this->title,
'infoWindow' => $this->infoWindow?->toArray(),
'extra' => (object) $this->extra,
'extra' => $this->extra,
];
}

Expand Down
52 changes: 52 additions & 0 deletions src/Map/tests/DummyOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Map\Tests;

use Symfony\UX\Map\MapOptionsInterface;
use Symfony\UX\Map\MapOptionsNormalizer;

final readonly class DummyOptions implements MapOptionsInterface
{
public function __construct(
private string $mapId,
private string $mapType,
) {
}

public static function registerToNormalizer(): void
{
MapOptionsNormalizer::$providers['dummy'] = self::class;
}

public static function unregisterFromNormalizer(): void
{
unset(MapOptionsNormalizer::$providers['dummy']);
}

public static function fromArray(array $array): MapOptionsInterface
{
return new self(
$array['mapId'],
$array['mapType'],
);
}

public function toArray(): array
{
return [
'mapId' => $this->mapId,
'mapType' => $this->mapType,
];
}
}
Loading

0 comments on commit 1fe568b

Please sign in to comment.