[Map] Create Map component

This commit is contained in:
Hugo Alliaume
2024-06-10 23:06:54 +02:00
committed by Javier Eguiluz
commit aef37d501a
53 changed files with 2639 additions and 0 deletions

8
.gitattributes vendored Normal file
View File

@@ -0,0 +1,8 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/.symfony.bundle.yaml export-ignore
/phpunit.xml.dist export-ignore
/assets/src export-ignore
/assets/test export-ignore
/assets/vitest.config.js export-ignore
/tests export-ignore

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vendor
composer.lock
.phpunit.result.cache

3
.symfony.bundle.yaml Normal file
View File

@@ -0,0 +1,3 @@
branches: ["2.x"]
maintained_branches: ["2.x"]
doc_dir: "doc"

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# CHANGELOG
## Unreleased
- Component added

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2024-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# Symfony UX Map
**EXPERIMENTAL** This component is currently experimental and is
likely to change, or even change drastically.
Symfony UX Map integrates interactive Maps in Symfony applications, like Leaflet or Google Maps.
**This repository is a READ-ONLY sub-tree split**. See
https://github.com/symfony/ux to create issues or submit pull requests.
## Resources
- [Documentation](https://symfony.com/bundles/ux-map/current/index.html)
- [Report issues](https://github.com/symfony/ux/issues) and
[send Pull Requests](https://github.com/symfony/ux/pulls)
in the [main Symfony UX repository](https://github.com/symfony/ux)

View File

@@ -0,0 +1,55 @@
import { Controller } from '@hotwired/stimulus';
export type Point = {
lat: number;
lng: number;
};
export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
center: Point;
zoom: number;
fitBoundsToMarkers: boolean;
markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
options: Options;
};
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
position: Point;
title: string | null;
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
rawOptions?: MarkerOptions;
};
export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
position: Point;
opened: boolean;
autoClose: boolean;
rawOptions?: InfoWindowOptions;
};
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow> extends Controller<HTMLElement> {
static values: {
providerOptions: ObjectConstructor;
view: ObjectConstructor;
};
viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
protected map: Map;
protected markers: Array<Marker>;
protected infoWindows: Array<InfoWindow>;
initialize(): void;
connect(): void;
protected abstract doCreateMap({ center, zoom, options, }: {
center: Point;
zoom: number;
options: MapOptions;
}): Map;
createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
protected createInfoWindow({ definition, marker, }: {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
marker: Marker;
}): InfoWindow;
protected abstract doCreateInfoWindow({ definition, marker, }: {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
marker: Marker;
}): InfoWindow;
protected abstract doFitBoundsToMarkers(): void;
private dispatchEvent;
}

47
assets/dist/abstract_map_controller.js vendored Normal file
View File

@@ -0,0 +1,47 @@
import { Controller } from '@hotwired/stimulus';
class default_1 extends Controller {
constructor() {
super(...arguments);
this.markers = [];
this.infoWindows = [];
}
initialize() { }
connect() {
const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
this.dispatchEvent('pre-connect', { options });
this.map = this.doCreateMap({ center, zoom, options });
markers.forEach((marker) => this.createMarker(marker));
if (fitBoundsToMarkers) {
this.doFitBoundsToMarkers();
}
this.dispatchEvent('connect', {
map: this.map,
markers: this.markers,
infoWindows: this.infoWindows,
});
}
createMarker(definition) {
this.dispatchEvent('marker:before-create', { definition });
const marker = this.doCreateMarker(definition);
this.dispatchEvent('marker:after-create', { marker });
this.markers.push(marker);
return marker;
}
createInfoWindow({ definition, marker, }) {
this.dispatchEvent('info-window:before-create', { definition, marker });
const infoWindow = this.doCreateInfoWindow({ definition, marker });
this.dispatchEvent('info-window:after-create', { infoWindow, marker });
this.infoWindows.push(infoWindow);
return infoWindow;
}
dispatchEvent(name, payload = {}) {
this.dispatch(name, { prefix: 'ux:map', detail: payload });
}
}
default_1.values = {
providerOptions: Object,
view: Object,
};
export { default_1 as default };

21
assets/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "@symfony/ux-map",
"description": "Integrates interactive maps in your Symfony applications",
"license": "MIT",
"version": "1.0.0",
"type": "module",
"main": "dist/abstract_map_controller.js",
"types": "dist/abstract_map_controller.d.ts",
"symfony": {
"importmap": {
"@hotwired/stimulus": "^3.0.0",
"@symfony/ux-map/abstract-map-controller": "path:%PACKAGE%/dist/abstract_map_controller.js"
}
},
"peerDependencies": {
"@hotwired/stimulus": "^3.0.0"
},
"devDependencies": {
"@hotwired/stimulus": "^3.0.0"
}
}

View File

@@ -0,0 +1,121 @@
import { Controller } from '@hotwired/stimulus';
export type Point = { lat: number; lng: number };
export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
center: Point;
zoom: number;
fitBoundsToMarkers: boolean;
markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
options: Options;
};
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
position: Point;
title: string | null;
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
rawOptions?: MarkerOptions;
};
export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
position: Point;
opened: boolean;
autoClose: boolean;
rawOptions?: InfoWindowOptions;
};
export default abstract class<
MapOptions,
Map,
MarkerOptions,
Marker,
InfoWindowOptions,
InfoWindow,
> extends Controller<HTMLElement> {
static values = {
providerOptions: Object,
view: Object,
};
declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
protected map: Map;
protected markers: Array<Marker> = [];
protected infoWindows: Array<InfoWindow> = [];
initialize() {}
connect() {
const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
this.dispatchEvent('pre-connect', { options });
this.map = this.doCreateMap({ center, zoom, options });
markers.forEach((marker) => this.createMarker(marker));
if (fitBoundsToMarkers) {
this.doFitBoundsToMarkers();
}
this.dispatchEvent('connect', {
map: this.map,
markers: this.markers,
infoWindows: this.infoWindows,
});
}
protected abstract doCreateMap({
center,
zoom,
options,
}: {
center: Point;
zoom: number;
options: MapOptions;
}): Map;
public createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker {
this.dispatchEvent('marker:before-create', { definition });
const marker = this.doCreateMarker(definition);
this.dispatchEvent('marker:after-create', { marker });
this.markers.push(marker);
return marker;
}
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
protected createInfoWindow({
definition,
marker,
}: {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
marker: Marker;
}): InfoWindow {
this.dispatchEvent('info-window:before-create', { definition, marker });
const infoWindow = this.doCreateInfoWindow({ definition, marker });
this.dispatchEvent('info-window:after-create', { infoWindow, marker });
this.infoWindows.push(infoWindow);
return infoWindow;
}
protected abstract doCreateInfoWindow({
definition,
marker,
}: {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
marker: Marker;
}): InfoWindow;
protected abstract doFitBoundsToMarkers(): void;
private dispatchEvent(name: string, payload: Record<string, unknown> = {}): void {
this.dispatch(name, { prefix: 'ux:map', detail: payload });
}
}

View File

@@ -0,0 +1,76 @@
import { Application } from '@hotwired/stimulus';
import { getByTestId, waitFor } from '@testing-library/dom';
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
import AbstractMapController from '../src/abstract_map_controller.ts';
class MyMapController extends AbstractMapController {
doCreateMap({ center, zoom, options }) {
return { map: 'map', center, zoom, options };
}
doCreateMarker(definition) {
const marker = { marker: 'marker', title: definition.title };
if (definition.infoWindow) {
this.createInfoWindow({ definition: definition.infoWindow, marker });
}
return marker;
}
doCreateInfoWindow({ definition, marker }) {
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: marker.title };
}
doFitBoundsToMarkers() {
// no-op
}
}
const startStimulus = () => {
const application = Application.start();
application.register('map', MyMapController);
return application;
};
describe('AbstractMapController', () => {
let container: HTMLElement;
beforeEach(() => {
container = mountDOM(`
<div
data-testid="map"
data-controller="map"
style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px"
data-map-provider-options-value="&#x7B;&#x7D;"
data-map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&#x7D;,&quot;markers&quot;&#x3A;&#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&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
></div>
`);
});
afterEach(() => {
clearDOM();
});
it('connect and create map, marker and info window', async () => {
const div = getByTestId(container, 'map');
expect(div).not.toHaveClass('connected');
const application = startStimulus();
await waitFor(() => expect(application.getControllerForElementAndIdentifier(div, 'map')).not.toBeNull());
const controller = application.getControllerForElementAndIdentifier(div, 'map');
expect(controller.map).toEqual({ map: 'map', center: { lat: 48.8566, lng: 2.3522 }, zoom: 4, options: {} });
expect(controller.markers).toEqual([
{ marker: 'marker', title: 'Paris' },
{ marker: 'marker', title: 'Lyon' },
]);
expect(controller.infoWindows).toEqual([
{
headerContent: '<b>Lyon</b>',
infoWindow: 'infoWindow',
marker: 'Lyon',
},
]);
});
});

51
composer.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "symfony/ux-map",
"type": "symfony-bundle",
"description": "Easily embed interactive maps in your Symfony application",
"keywords": [
"symfony-ux",
"map",
"markers",
"maps"
],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Hugo Alliaume",
"email": "hugo@alliau.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"autoload": {
"psr-4": {
"Symfony\\UX\\Map\\": "src/"
},
"exclude-from-classmap": []
},
"autoload-dev": {
"psr-4": {
"Symfony\\UX\\Map\\Tests\\": "tests/"
}
},
"require": {
"php": ">=8.3",
"symfony/stimulus-bundle": "^2.18.1"
},
"require-dev": {
"symfony/asset-mapper": "^6.4|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/twig-bundle": "^6.4|^7.0"
},
"extra": {
"thanks": {
"name": "symfony/ux",
"url": "https://github.com/symfony/ux"
}
},
"minimum-stability": "dev"
}

45
config/services.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
use Symfony\UX\Map\Renderer\Renderer;
use Symfony\UX\Map\Renderer\Renderers;
use Symfony\UX\Map\Twig\MapExtension;
/*
* @author Hugo Alliaume <hugo@alliau.me>
*/
return static function (ContainerConfigurator $container): void {
$container->services()
->set('ux_map.renderers', Renderers::class)
->factory([service('ux_map.renderer_factory'), 'fromStrings'])
->args([
abstract_arg('renderers configuration'),
])
->tag('twig.runtime')
->set('ux_map.renderer_factory.abstract', AbstractRendererFactory::class)
->abstract()
->args([
service('stimulus.helper'),
])
->set('ux_map.renderer_factory', Renderer::class)
->args([
tagged_iterator('ux_map.renderer_factory'),
])
->set('ux_map.twig_extension', MapExtension::class)
->tag('twig.extension')
;
};

207
doc/index.rst Normal file
View File

@@ -0,0 +1,207 @@
Symfony UX Map
==============
**EXPERIMENTAL** This component is currently experimental and is likely
to change, or even change drastically.
Symfony UX Map is a Symfony bundle integrating interactive Maps in
Symfony applications. It is part of `the Symfony UX initiative`_.
Installation
------------
.. caution::
Before you start, make sure you have `StimulusBundle configured in your app`_.
Install the bundle using Composer and Symfony Flex:
.. code-block:: terminal
$ composer require symfony/ux-map
If you're using WebpackEncore, install your assets and restart Encore (not
needed if you're using AssetMapper):
.. code-block:: terminal
$ npm install --force
$ npm run watch
Configuration
-------------
Configuration is done in your ``config/packages/ux_map.yaml`` file:
.. code-block:: yaml
# config/packages/ux_map.yaml
ux_map:
renderer: '%env(UX_MAP_DSN)%'
The ``UX_MAP_DSN`` environment variable configure which renderer to use.
Available renderers
~~~~~~~~~~~~~~~~~~~
========== ===============================================================
Renderer
========== ===============================================================
`Google`_ **Install**: ``composer require symfony/ux-map-google`` \
**DSN**: ``UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default`` \
`Leaflet`_ **Install**: ``composer require symfony/ux-map-leaflet`` \
**DSN**: ``UX_MAP_DSN=leaflet://default`` \
========== ===============================================================
Usage
-----
Creating and rendering
~~~~~~~~~~~~~~~~~~~~~~
A map is created by calling ``new Map()``. You can configure the center, zoom, and add markers::
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\UX\Map\InfoWindow;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Marker;
use Symfony\UX\Map\Point;
final class HomeController extends AbstractController
{
#[Route('/')]
public function __invoke(): Response
{
// 1. Create a new map instance
$myMap = (new Map());
->center(new Point(46.903354, 1.888334))
->zoom(6)
;
// 2. You can add markers, with an optional info window
$myMap
->addMarker(new Marker(
position: new Point(48.8566, 2.3522),
title: 'Paris'
))
->addMarker(new Marker(
position: new Point(45.7640, 4.8357),
title: 'Lyon',
// With an info window
infoWindow: new InfoWindow(
headerContent: '<b>Lyon</b>',
content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.'
)
));
// 3. And inject the map in your template to render it
return $this->render('contact/index.html.twig', [
'my_map' => $myMap,
]);
}
}
To render a map in your Twig template, use the ``render_map`` Twig function, e.g.:
.. code-block:: twig
{{ render_map(my_map) }}
{# or with custom attributes #}
{{ render_map(my_map, { style: 'height: 300px' }) }}
Extend the default behavior
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Symfony UX Map allows you to extend its default behavior using a custom Stimulus controller:
.. code-block:: javascript
// assets/controllers/mymap_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.element.addEventListener('ux:map:pre-connect', this._onPreConnect);
this.element.addEventListener('ux:map:connect', this._onConnect);
this.element.addEventListener('ux:map:marker:before-create', this._onMarkerBeforeConnect);
this.element.addEventListener('ux:map:marker:after-create', this._onMarkerAfterCreate);
this.element.addEventListener('ux:map:info-window:before-create', this._onInfoWindowBeforeConnect);
this.element.addEventListener('ux:map:info-window:after-create', this._onInfoWindowAfterCreate);
}
disconnect() {
// You should always remove listeners when the controller is disconnected to avoid side effects
this.element.removeEventListener('ux:map:pre-connect', this._onPreConnect);
this.element.removeEventListener('ux:map:connect', this._onConnect);
this.element.removeEventListener('ux:map:marker:before-create', this._onMarkerBeforeConnect);
this.element.removeEventListener('ux:map:marker:after-create', this._onMarkerAfterCreate);
this.element.removeEventListener('ux:map:info-window:before-create', this._onInfoWindowBeforeConnect);
this.element.removeEventListener('ux:map:info-window:after-create', this._onInfoWindowAfterCreate);
}
_onPreConnect(event) {
// The map is not created yet
// You can use this event to configure the map before it is created
console.log(event.detail.options);
}
_onConnect(event) {
// The map, markers and infoWindows are created
// The instances depend on the renderer you are using
console.log(event.detail.map);
console.log(event.detail.markers);
console.log(event.detail.infoWindows);
}
_onMarkerBeforeConnect(event) {
// The marker is not created yet
// You can use this event to configure the marker before it is created
console.log(event.detail.definition);
}
_onMarkerAfterCreate(event) {
// The marker is created
// The instance depends on the renderer you are using
console.log(event.detail.marker);
}
_onInfoWindowBeforeConnect(event) {
// The infoWindow is not created yet
// You can use this event to configure the infoWindow before it is created
console.log(event.detail.definition);
// The associated marker instance is also available
console.log(event.detail.marker);
}
_onInfoWindowAfterCreate(event) {
// The infoWindow is created
// The instance depends on the renderer you are using
console.log(event.detail.infoWindow);
// The associated marker instance is also available
console.log(event.detail.marker);
}
}
Then, you can use this controller in your template:
.. code-block:: twig
{{ render_map(my_map, { 'data-controller': 'mymap', style: 'height: 300px' }) }}
Backward Compatibility promise
------------------------------
This bundle aims at following the same Backward Compatibility promise as
the Symfony framework:
https://symfony.com/doc/current/contributing/code/bc.html
.. _`the Symfony UX initiative`: https://symfony.com/ux
.. _StimulusBundle configured in your app: https://symfony.com/bundles/StimulusBundle/current/index.html
.. _`Google`: https://github.com/symfony/symfony-ux/blob/{version}/src/Map/src/Bridge/Google/README.md
.. _`Leaflet`: https://github.com/symfony/symfony-ux/blob/{version}/src/Map/src/Bridge/Leaflet/README.md

26
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
</php>
<testsuites>
<testsuite name="Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Exception;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
interface Exception extends \Throwable
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Exception;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
final class IncompleteDsnException extends InvalidArgumentException
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Exception;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Exception;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
class LogicException extends \LogicException implements Exception
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Exception;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
class RuntimeException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Renderer\Dsn;
use Symfony\UX\Map\UXMapBundle;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
class UnsupportedSchemeException extends InvalidArgumentException
{
public function __construct(Dsn $dsn, ?\Throwable $previous = null)
{
$provider = $dsn->getScheme();
$bridge = UXMapBundle::$bridges[$provider] ?? null;
if ($bridge && !class_exists($bridge['renderer_factory'])) {
parent::__construct(\sprintf('Unable to render maps via "%s" as the bridge is not installed. Try running "composer require symfony/ux-map-%s".', $provider, $provider));
return;
}
parent::__construct(
\sprintf('The renderer "%s" is not supported.', $dsn->getScheme()),
0,
$previous
);
}
}

40
src/InfoWindow.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map;
/**
* Represents an information window that can be displayed on a map.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
final readonly class InfoWindow
{
public function __construct(
private ?string $headerContent = null,
private ?string $content = null,
private ?Point $position = null,
private bool $opened = false,
private bool $autoClose = true,
) {
}
public function toArray(): array
{
return [
'headerContent' => $this->headerContent,
'content' => $this->content,
'position' => $this->position?->toArray(),
'opened' => $this->opened,
'autoClose' => $this->autoClose,
];
}
}

104
src/Map.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Exception\InvalidArgumentException;
/**
* Represents a map.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
final class Map
{
public function __construct(
private readonly ?string $rendererName = null,
private ?MapOptionsInterface $options = null,
private ?Point $center = null,
private ?float $zoom = null,
private bool $fitBoundsToMarkers = false,
/**
* @var array<Marker>
*/
private array $markers = [],
) {
}
public function getRendererName(): ?string
{
return $this->rendererName;
}
public function center(Point $center): self
{
$this->center = $center;
return $this;
}
public function zoom(float $zoom): self
{
$this->zoom = $zoom;
return $this;
}
public function fitBoundsToMarkers(bool $enable = true): self
{
$this->fitBoundsToMarkers = $enable;
return $this;
}
public function options(MapOptionsInterface $options): self
{
$this->options = $options;
return $this;
}
public function getOptions(): ?MapOptionsInterface
{
return $this->options;
}
public function hasOptions(): bool
{
return null !== $this->options;
}
public function addMarker(Marker $marker): self
{
$this->markers[] = $marker;
return $this;
}
public function toArray(): array
{
if (null === $this->center) {
throw new InvalidArgumentException('The center of the map must be set.');
}
if (null === $this->zoom) {
throw new InvalidArgumentException('The zoom of the map must be set.');
}
return [
'center' => $this->center->toArray(),
'zoom' => $this->zoom,
'fitBoundsToMarkers' => $this->fitBoundsToMarkers,
'options' => (object) ($this->options?->toArray() ?? []),
'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
interface MapOptionsInterface
{
/**
* @return array<string, mixed>
*/
public function toArray(): array;
}

36
src/Marker.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map;
/**
* Represents a marker on a map.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
final readonly class Marker
{
public function __construct(
private Point $position,
private ?string $title = null,
private ?InfoWindow $infoWindow = null,
) {
}
public function toArray(): array
{
return [
'position' => $this->position->toArray(),
'title' => $this->title,
'infoWindow' => $this->infoWindow?->toArray(),
];
}
}

46
src/Point.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Exception\InvalidArgumentException;
/**
* Represents a geographical point.
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
final readonly class Point
{
public function __construct(
public float $latitude,
public float $longitude,
) {
if ($latitude < -90 || $latitude > 90) {
throw new InvalidArgumentException(\sprintf('Latitude must be between -90 and 90 degrees, "%s" given.', $latitude));
}
if ($longitude < -180 || $longitude > 180) {
throw new InvalidArgumentException(\sprintf('Longitude must be between -180 and 180 degrees, "%s" given.', $longitude));
}
}
/**
* @return array{lat: float, lng: float}
*/
public function toArray(): array
{
return [
'lat' => $this->latitude,
'lng' => $this->longitude,
];
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\MapOptionsInterface;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
abstract readonly class AbstractRenderer implements RendererInterface
{
public function __construct(
private StimulusHelper $stimulus,
) {
}
abstract protected function getName(): string;
abstract protected function getProviderOptions(): array;
abstract protected function getDefaultMapOptions(): MapOptionsInterface;
final public function renderMap(Map $map, array $attributes = []): string
{
if (!$map->hasOptions()) {
$map->options($this->getDefaultMapOptions());
} elseif (!$map->getOptions() instanceof ($defaultMapOptions = $this->getDefaultMapOptions())) {
$map->options($defaultMapOptions);
}
$stimulusAttributes = $this->stimulus->createStimulusAttributes();
foreach ($attributes as $name => $value) {
if ('data-controller' === $name) {
continue;
}
if (true === $value) {
$stimulusAttributes->addAttribute($name, $name);
} elseif (false !== $value) {
$stimulusAttributes->addAttribute($name, $value);
}
}
$stimulusAttributes->addController(
'@symfony/ux-map-'.$this->getName().'/map',
[
'provider-options' => (object) $this->getProviderOptions(),
'view' => $map->toArray(),
]
);
return \sprintf('<div %s></div>', $stimulusAttributes);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Exception\IncompleteDsnException;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
abstract class AbstractRendererFactory
{
public function __construct(
protected StimulusHelper $stimulus,
) {
}
public function supports(Dsn $dsn): bool
{
return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true);
}
protected function getUser(Dsn $dsn): string
{
return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.');
}
/**
* @return string[]
*/
abstract protected function getSupportedSchemes(): array;
}

81
src/Renderer/Dsn.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Exception\InvalidArgumentException;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
final readonly class Dsn
{
private string $scheme;
private string $host;
private ?string $user;
private array $options;
private string $originalDsn;
public function __construct(#[\SensitiveParameter] string $dsn)
{
$this->originalDsn = $dsn;
if (false === $params = parse_url($dsn)) {
throw new InvalidArgumentException('The map renderer DSN is invalid.');
}
if (!isset($params['scheme'])) {
throw new InvalidArgumentException('The map renderer DSN must contain a scheme.');
}
$this->scheme = $params['scheme'];
if (!isset($params['host'])) {
throw new InvalidArgumentException('The map renderer DSN must contain a host (use "default" by default).');
}
$this->host = $params['host'];
$this->user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null;
$options = [];
parse_str($params['query'] ?? '', $options);
$this->options = $options;
}
public function getScheme(): string
{
return $this->scheme;
}
public function getHost(): string
{
return $this->host;
}
public function getUser(): ?string
{
return $this->user;
}
public function getOption(string $key, mixed $default = null): mixed
{
return $this->options[$key] ?? $default;
}
public function getOptions(): array
{
return $this->options;
}
public function getOriginalDsn(): string
{
return $this->originalDsn;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Exception\LogicException;
use Symfony\UX\Map\Map;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
final readonly class NullRenderer implements RendererInterface
{
public function __construct(
private array $availableBridges = [],
) {
}
public function renderMap(Map $map, array $attributes = []): string
{
$message = 'You must install at least one bridge package to use the Symfony UX Map component.';
if ($this->availableBridges) {
$message .= \PHP_EOL.'Try running '.implode(' or ', array_map(fn ($name) => \sprintf('"composer require %s"', $name), $this->availableBridges)).'.';
}
throw new LogicException($message);
}
public function __toString(): string
{
return 'null://null';
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Exception\UnsupportedSchemeException;
final readonly class NullRendererFactory implements RendererFactoryInterface
{
/**
* @param array<string> $availableBridges
*/
public function __construct(
private array $availableBridges = [],
) {
}
public function create(Dsn $dsn): RendererInterface
{
if (!$this->supports($dsn)) {
throw new UnsupportedSchemeException($dsn);
}
return new NullRenderer($this->availableBridges);
}
public function supports(Dsn $dsn): bool
{
return 'null' === $dsn->getScheme();
}
}

56
src/Renderer/Renderer.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Exception\UnsupportedSchemeException;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
final readonly class Renderer
{
public function __construct(
/**
* @param iterable<RendererFactoryInterface> $factories
*/
private iterable $factories,
) {
}
public function fromStrings(#[\SensitiveParameter] array $dsns): Renderers
{
$renderers = [];
foreach ($dsns as $name => $dsn) {
$renderers[$name] = $this->fromString($dsn);
}
return new Renderers($renderers);
}
public function fromString(#[\SensitiveParameter] string $dsn): RendererInterface
{
return $this->fromDsnObject(new Dsn($dsn));
}
public function fromDsnObject(Dsn $dsn): RendererInterface
{
foreach ($this->factories as $factory) {
if ($factory->supports($dsn)) {
return $factory->create($dsn);
}
}
throw new UnsupportedSchemeException($dsn);
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
interface RendererFactoryInterface
{
public function create(Dsn $dsn): RendererInterface;
public function supports(Dsn $dsn): bool;
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Map;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
interface RendererInterface extends \Stringable
{
/**
* @param array<string, string|bool> $attributes an array of HTML attributes
*/
public function renderMap(Map $map, array $attributes = []): string;
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Renderer;
use Symfony\UX\Map\Exception\LogicException;
use Symfony\UX\Map\Map;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
final class Renderers implements RendererInterface
{
/**
* @var array<string, RendererInterface>
*/
private array $renderers = [];
private RendererInterface $default;
/**
* @param iterable<string, RendererInterface> $renderers
*/
public function __construct(iterable $renderers)
{
foreach ($renderers as $name => $renderer) {
$this->default ??= $renderer;
$this->renderers[$name] = $renderer;
}
if (!$this->renderers) {
throw new LogicException(\sprintf('"%s" must have at least one renderer configured.', __CLASS__));
}
}
public function renderMap(Map $map, array $attributes = []): string
{
$renderer = $this->default;
if ($rendererName = $map->getRendererName()) {
if (!isset($this->renderers[$rendererName])) {
throw new LogicException(\sprintf('The "%s" renderer does not exist (available renderers: "%s").', $rendererName, implode('", "', array_keys($this->renderers))));
}
$renderer = $this->renderers[$rendererName];
}
return $renderer->renderMap($map, $attributes);
}
public function __toString()
{
return implode(', ', array_keys($this->renderers));
}
}

View File

@@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Test;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\UnsupportedSchemeException;
use Symfony\UX\Map\Renderer\Dsn;
use Symfony\UX\Map\Renderer\RendererFactoryInterface;
/**
* A test case to ease testing a renderer factory.
*
* @author Oskar Stark <oskarstark@googlemail.com>
* @author Hugo Alliaume <hugo@alliau.me>
*/
abstract class RendererFactoryTestCase extends TestCase
{
abstract public function createRendererFactory(): RendererFactoryInterface;
/**
* @return iterable<array{0: bool, 1: string}>
*/
abstract public static function supportsRenderer(): iterable;
/**
* @return iterable<array{0: string, 1: string}>
*/
abstract public static function createRenderer(): iterable;
/**
* @return iterable<array{0: string, 1: string|null}>
*/
public static function unsupportedSchemeRenderer(): iterable
{
return [];
}
/**
* @return iterable<array{0: string, 1: string|null}>
*/
public static function incompleteDsnRenderer(): iterable
{
return [];
}
/**
* @dataProvider supportsRenderer
*/
public function testSupports(bool $expected, string $dsn): void
{
$factory = $this->createRendererFactory();
$this->assertSame($expected, $factory->supports(new Dsn($dsn)));
}
/**
* @dataProvider createRenderer
*/
public function testCreate(string $expected, string $dsn): void
{
$factory = $this->createRendererFactory();
$renderer = $factory->create(new Dsn($dsn));
$this->assertSame($expected, (string) $renderer);
}
/**
* @dataProvider unsupportedSchemeRenderer
*/
public function testUnsupportedSchemeException(string $dsn, ?string $message = null): void
{
$factory = $this->createRendererFactory();
$dsn = new Dsn($dsn);
$this->expectException(UnsupportedSchemeException::class);
if (null !== $message) {
$this->expectExceptionMessage($message);
}
$factory->create($dsn);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Test;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Renderer\RendererInterface;
/**
* A test case to ease testing a renderer.
*/
abstract class RendererTestCase extends TestCase
{
/**
* @return iterable<array{expected_render: string, renderer: RendererInterface, map: Map, attributes: array<mixed>}>
*/
abstract public function provideTestRenderMap(): iterable;
/**
* @dataProvider provideTestRenderMap
*/
public function testRenderMap(string $expectedRender, RendererInterface $renderer, Map $map, array $attributes = []): void
{
self::assertSame($expectedRender, $renderer->renderMap($map, $attributes));
}
}

29
src/Twig/MapExtension.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Twig;
use Symfony\UX\Map\Renderer\Renderers;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
final class MapExtension extends AbstractExtension
{
public function getFunctions(): iterable
{
yield new TwigFunction('render_map', [Renderers::class, 'renderMap'], ['is_safe' => ['html']]);
}
}

109
src/UXMapBundle.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
use Symfony\UX\Map\Bridge as MapBridge;
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
use Symfony\UX\Map\Renderer\NullRendererFactory;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
final class UXMapBundle extends AbstractBundle
{
protected string $extensionAlias = 'ux_map';
/**
* @var array<string, array{ renderer_factory: class-string<AbstractRendererFactory> }>
*
* @internal
*/
public static array $bridges = [
'google' => ['renderer_factory' => MapBridge\Google\Renderer\GoogleRendererFactory::class],
'leaflet' => ['renderer_factory' => MapBridge\Leaflet\Renderer\LeafletRendererFactory::class],
];
public function configure(DefinitionConfigurator $definition): void
{
$rootNode = $definition->rootNode();
$rootNode
->children()
->scalarNode('renderer')->defaultNull()->end()
->end()
;
}
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$container->import('../config/services.php');
if (!isset($config['renderer'])) {
$config['renderer'] = 'null://null';
}
if (str_starts_with($config['renderer'], 'null://')) {
$container->services()
->set('ux_map.renderer_factory.null', NullRendererFactory::class)
->arg(0, array_map(fn ($name) => 'symfony/ux-map-'.$name, array_keys(self::$bridges)))
->tag('ux_map.renderer_factory');
}
$renderers = ['default' => $config['renderer']];
$container->services()
->get('ux_map.renderers')
->arg(0, $renderers);
foreach (self::$bridges as $name => $bridge) {
if (ContainerBuilder::willBeAvailable('symfony/ux-map-'.$name, $bridge['renderer_factory'], ['symfony/ux-map'])) {
$container->services()
->set('ux_map.renderer_factory.'.$name, $bridge['renderer_factory'])
->parent('ux_map.renderer_factory.abstract')
->tag('ux_map.renderer_factory');
}
}
}
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
{
if (!$this->isAssetMapperAvailable()) {
return;
}
$paths = [
__DIR__.'/../assets/dist' => '@symfony/ux-map',
];
foreach (self::$bridges as $name => $bridge) {
if (ContainerBuilder::willBeAvailable('symfony/ux-map-'.$name, $bridge['renderer_factory'], ['symfony/ux-map'])) {
$rendererFactoryReflection = new \ReflectionClass($bridge['renderer_factory']);
$bridgePath = \dirname($rendererFactoryReflection->getFileName(), 3);
$paths[$bridgePath.'/assets/dist'] = '@symfony/ux-map-'.$name;
}
}
$builder->prependExtensionConfig('framework', [
'asset_mapper' => [
'paths' => $paths,
],
]);
}
private function isAssetMapperAvailable(): bool
{
return interface_exists(AssetMapperInterface::class);
}
}

41
tests/InfoWindowTest.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 PHPUnit\Framework\TestCase;
use Symfony\UX\Map\InfoWindow;
use Symfony\UX\Map\Point;
class InfoWindowTest extends TestCase
{
public function testToArray(): void
{
$infoWindow = new InfoWindow(
headerContent: 'Paris',
content: 'Capitale de la France, est une grande ville européenne et un centre mondial de l\'art, de la mode, de la gastronomie et de la culture.',
position: new Point(48.8566, 2.3522),
opened: true,
autoClose: false,
);
self::assertSame([
'headerContent' => 'Paris',
'content' => 'Capitale de la France, est une grande ville européenne et un centre mondial de l\'art, de la mode, de la gastronomie et de la culture.',
'position' => [
'lat' => 48.8566,
'lng' => 2.3522,
],
'opened' => true,
'autoClose' => false,
], $infoWindow->toArray());
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Kernel;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
trait AppKernelTrait
{
public function getCacheDir(): string
{
return $this->createTmpDir('cache');
}
public function getLogDir(): string
{
return $this->createTmpDir('logs');
}
private function createTmpDir(string $type): string
{
$dir = sys_get_temp_dir().'/map_bundle/'.uniqid($type.'_', true);
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
return $dir;
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Kernel;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\UX\Map\UXMapBundle;
use Symfony\UX\StimulusBundle\StimulusBundle;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
class FrameworkAppKernel extends Kernel
{
use AppKernelTrait;
public function registerBundles(): iterable
{
return [new FrameworkBundle(), new StimulusBundle(), new UXMapBundle()];
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container) {
$container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
$container->loadFromExtension('ux_map', []);
$container->setAlias('test.ux_map.renderers', 'ux_map.renderers')->setPublic(true);
});
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Kernel;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\UX\Map\UXMapBundle;
use Symfony\UX\StimulusBundle\StimulusBundle;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
class TwigAppKernel extends Kernel
{
use AppKernelTrait;
public function registerBundles(): iterable
{
return [new FrameworkBundle(), new StimulusBundle(), new TwigBundle(), new UXMapBundle()];
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container) {
$container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
$container->loadFromExtension('twig', ['default_path' => __DIR__.'/templates', 'strict_variables' => true, 'exception_controller' => null]);
$container->loadFromExtension('ux_map', []);
$container->setAlias('test.ux_map.renderers', 'ux_map.renderers')->setPublic(true);
});
}
}

121
tests/MapTest.php Normal file
View File

@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\InvalidArgumentException;
use Symfony\UX\Map\InfoWindow;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\MapOptionsInterface;
use Symfony\UX\Map\Marker;
use Symfony\UX\Map\Point;
class MapTest extends TestCase
{
public function testCenterValidation(): void
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('The center of the map must be set.');
$map = new Map();
$map->toArray();
}
public function testZoomValidation(): void
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage('The zoom of the map must be set.');
$map = new Map(
center: new Point(48.8566, 2.3522)
);
$map->toArray();
}
public function testWithMinimumConfiguration(): void
{
$map = new Map();
$map
->center(new Point(48.8566, 2.3522))
->zoom(6);
$array = $map->toArray();
self::assertSame([
'center' => ['lat' => 48.8566, 'lng' => 2.3522],
'zoom' => 6.0,
'fitBoundsToMarkers' => false,
'options' => $array['options'],
'markers' => [],
], $array);
}
public function testWithMaximumConfiguration(): void
{
$map = new Map();
$map
->center(new Point(48.8566, 2.3522))
->zoom(6)
->fitBoundsToMarkers()
->options(new class() implements MapOptionsInterface {
public function toArray(): array
{
return [
'mapTypeId' => 'roadmap',
];
}
})
->addMarker(new Marker(
position: new Point(48.8566, 2.3522),
title: 'Paris',
infoWindow: new InfoWindow(headerContent: '<b>Paris</b>', content: 'Paris', position: new Point(48.8566, 2.3522))
))
->addMarker(new Marker(
position: new Point(45.764, 4.8357),
title: 'Lyon',
infoWindow: new InfoWindow(headerContent: '<b>Lyon</b>', content: 'Lyon', position: new Point(45.764, 4.8357), opened: true)
))
->addMarker(new Marker(
position: new Point(43.2965, 5.3698),
title: 'Marseille',
infoWindow: new InfoWindow(headerContent: '<b>Marseille</b>', content: 'Marseille', position: new Point(43.2965, 5.3698), opened: true)
));
$array = $map->toArray();
self::assertSame([
'center' => ['lat' => 48.8566, 'lng' => 2.3522],
'zoom' => 6.0,
'fitBoundsToMarkers' => true,
'options' => $array['options'],
'markers' => [
[
'position' => ['lat' => 48.8566, 'lng' => 2.3522],
'title' => 'Paris',
'infoWindow' => ['headerContent' => '<b>Paris</b>', 'content' => 'Paris', 'position' => ['lat' => 48.8566, 'lng' => 2.3522], 'opened' => false, 'autoClose' => true],
],
[
'position' => ['lat' => 45.764, 'lng' => 4.8357],
'title' => 'Lyon',
'infoWindow' => ['headerContent' => '<b>Lyon</b>', 'content' => 'Lyon', 'position' => ['lat' => 45.764, 'lng' => 4.8357], 'opened' => true, 'autoClose' => true],
],
[
'position' => ['lat' => 43.2965, 'lng' => 5.3698],
'title' => 'Marseille',
'infoWindow' => ['headerContent' => '<b>Marseille</b>', 'content' => 'Marseille', 'position' => ['lat' => 43.2965, 'lng' => 5.3698], 'opened' => true, 'autoClose' => true],
],
],
], $array);
self::assertSame('roadmap', $array['options']->mapTypeId);
}
}

55
tests/MarkerTest.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 PHPUnit\Framework\TestCase;
use Symfony\UX\Map\InfoWindow;
use Symfony\UX\Map\Marker;
use Symfony\UX\Map\Point;
class MarkerTest extends TestCase
{
public function testToArray(): void
{
$marker = new Marker(
position: new Point(48.8566, 2.3522),
);
self::assertSame([
'position' => ['lat' => 48.8566, 'lng' => 2.3522],
'title' => null,
'infoWindow' => null,
], $marker->toArray());
$marker = new Marker(
position: new Point(48.8566, 2.3522),
title: 'Paris',
infoWindow: new InfoWindow(
headerContent: '<b>Paris</b>',
content: "Capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.",
opened: true,
),
);
self::assertSame([
'position' => ['lat' => 48.8566, 'lng' => 2.3522],
'title' => 'Paris',
'infoWindow' => [
'headerContent' => '<b>Paris</b>',
'content' => "Capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.",
'position' => null,
'opened' => true,
'autoClose' => true,
],
], $marker->toArray());
}
}

45
tests/PointTest.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\InvalidArgumentException;
use Symfony\UX\Map\Point;
class PointTest extends TestCase
{
public static function provideInvalidPoint(): iterable
{
yield [91, 0, 'Latitude must be between -90 and 90 degrees, "91" given.'];
yield [-91, 0, 'Latitude must be between -90 and 90 degrees, "-91" given.'];
yield [0, 181, 'Longitude must be between -180 and 180 degrees, "181" given.'];
yield [0, -181, 'Longitude must be between -180 and 180 degrees, "-181" given.'];
}
/**
* @dataProvider provideInvalidPoint
*/
public function testInvalidPoint(float $latitude, float $longitude, string $expectedExceptionMessage): void
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage($expectedExceptionMessage);
new Point($latitude, $longitude);
}
public function testToArray(): void
{
$point = new Point(48.8566, 2.3533);
self::assertSame(['lat' => 48.8566, 'lng' => 2.3533], $point->toArray());
}
}

103
tests/Renderer/DsnTest.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Renderer;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\InvalidArgumentException;
use Symfony\UX\Map\Renderer\Dsn;
final class DsnTest extends TestCase
{
/**
* @dataProvider constructDsn
*/
public function testConstruct(string $dsnString, string $scheme, string $host, ?string $user = null, array $options = [], ?string $path = null)
{
$dsn = new Dsn($dsnString);
self::assertSame($dsnString, $dsn->getOriginalDsn());
self::assertSame($scheme, $dsn->getScheme());
self::assertSame($host, $dsn->getHost());
self::assertSame($user, $dsn->getUser());
self::assertSame($options, $dsn->getOptions());
}
public static function constructDsn(): iterable
{
yield 'simple dsn' => [
'scheme://default',
'scheme',
'default',
];
yield 'simple dsn including @ sign, but no user/password/token' => [
'scheme://@default',
'scheme',
'default',
];
yield 'simple dsn including : sign and @ sign, but no user/password/token' => [
'scheme://:@default',
'scheme',
'default',
];
yield 'simple dsn including user, : sign and @ sign, but no password' => [
'scheme://user1:@default',
'scheme',
'default',
'user1',
];
yield 'dsn with user' => [
'scheme://u$er@default',
'scheme',
'default',
'u$er',
];
yield 'dsn with user, and custom option' => [
'scheme://u$er@default?api_key=MY_API_KEY',
'scheme',
'default',
'u$er',
[
'api_key' => 'MY_API_KEY',
],
'/channel',
];
}
/**
* @dataProvider invalidDsn
*/
public function testInvalidDsn(string $dsnString, string $exceptionMessage)
{
self::expectException(InvalidArgumentException::class);
self::expectExceptionMessage($exceptionMessage);
new Dsn($dsnString);
}
public static function invalidDsn(): iterable
{
yield [
'leaflet://',
'The map renderer DSN is invalid.',
];
yield [
'//default',
'The map renderer DSN must contain a scheme.',
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Renderer;
use Symfony\UX\Map\Renderer\NullRendererFactory;
use Symfony\UX\Map\Renderer\RendererFactoryInterface;
use Symfony\UX\Map\Test\RendererFactoryTestCase;
final class NullRendererFactoryTest extends RendererFactoryTestCase
{
public function createRendererFactory(): RendererFactoryInterface
{
return new NullRendererFactory();
}
public static function supportsRenderer(): iterable
{
yield [true, 'null://null'];
yield [true, 'null://foobar'];
yield [false, 'google://GOOGLE_MAPS_API_KEY@default'];
yield [false, 'leaflet://default'];
}
public static function createRenderer(): iterable
{
yield [
'null://null',
'null://null',
];
}
public static function unsupportedSchemeRenderer(): iterable
{
yield ['somethingElse://foo@default'];
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Renderer;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\LogicException;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Renderer\NullRenderer;
use Symfony\UX\Map\Renderer\RendererInterface;
final class NullRendererTest extends TestCase
{
public function provideTestRenderMap(): iterable
{
yield 'no bridges' => [
'expected_exception_message' => 'You must install at least one bridge package to use the Symfony UX Map component.',
'renderer' => new NullRenderer(),
];
yield 'one bridge' => [
'expected_exception_message' => 'You must install at least one bridge package to use the Symfony UX Map component.'
.\PHP_EOL.'Try running "composer require symfony/ux-map-leaflet".',
'renderer' => new NullRenderer(['symfony/ux-map-leaflet']),
];
yield 'two bridges' => [
'expected_exception_message' => 'You must install at least one bridge package to use the Symfony UX Map component.'
.\PHP_EOL.'Try running "composer require symfony/ux-map-leaflet" or "composer require symfony/ux-map-google".',
'renderer' => new NullRenderer(['symfony/ux-map-leaflet', 'symfony/ux-map-google']),
];
}
/**
* @dataProvider provideTestRenderMap
*/
public function testRenderMap(string $expectedExceptionMessage, RendererInterface $renderer): void
{
self::expectException(LogicException::class);
self::expectExceptionMessage($expectedExceptionMessage);
$renderer->renderMap(new Map(), []);
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Renderer;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\UnsupportedSchemeException;
use Symfony\UX\Map\Renderer\Renderer;
use Symfony\UX\Map\Renderer\RendererFactoryInterface;
use Symfony\UX\Map\Renderer\RendererInterface;
final class RendererTest extends TestCase
{
public function testUnsupportedSchemeException(): void
{
self::expectException(UnsupportedSchemeException::class);
self::expectExceptionMessage('The renderer "scheme" is not supported.');
$renderer = new Renderer([]);
$renderer->fromString('scheme://default');
}
public function testSupportedFactory(): void
{
$renderer = new Renderer([
'one' => $oneFactory = self::createMock(RendererFactoryInterface::class),
'two' => $twoFactory = self::createMock(RendererFactoryInterface::class),
]);
$oneFactory->expects(self::once())->method('supports')->willReturn(false);
$twoFactory->expects(self::once())->method('supports')->willReturn(true);
$twoFactory->expects(self::once())->method('create')->willReturn($twoRenderer = self::createMock(RendererInterface::class));
$renderer = $renderer->fromString('scheme://default');
self::assertSame($twoRenderer, $renderer);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Tests\Renderer;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Exception\LogicException;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Renderer\RendererInterface;
use Symfony\UX\Map\Renderer\Renderers;
class RenderersTest extends TestCase
{
public function testConstructWithoutRenderers(): void
{
self::expectException(LogicException::class);
self::expectExceptionMessage('"Symfony\UX\Map\Renderer\Renderers" must have at least one renderer configured.');
new Renderers([]);
}
public function testRenderMapWithDefaultRenderer(): void
{
$defaultRenderer = $this->createMock(RendererInterface::class);
$defaultRenderer->expects(self::once())->method('renderMap')->willReturn('<div data-controller="@symfony/ux-map-default"></div>');
$renderers = new Renderers(['default' => $defaultRenderer]);
self::assertSame('<div data-controller="@symfony/ux-map-default"></div>', $renderers->renderMap(new Map()));
}
public function testRenderMapWithCustomRenderer(): void
{
$defaultRenderer = $this->createMock(RendererInterface::class);
$defaultRenderer->expects(self::never())->method('renderMap');
$customRenderer = $this->createMock(RendererInterface::class);
$customRenderer->expects(self::once())->method('renderMap')->willReturn('<div data-controller="@symfony/ux-map-custom"></div>');
$renderers = new Renderers(['default' => $defaultRenderer, 'custom' => $customRenderer]);
$map = new Map(rendererName: 'custom');
self::assertSame('<div data-controller="@symfony/ux-map-custom"></div>', $renderers->renderMap($map));
}
public function testRenderMapWithUnknownRenderer(): void
{
self::expectException(LogicException::class);
self::expectExceptionMessage('The "unknown" renderer does not exist (available renderers: "default").');
$defaultRenderer = $this->createMock(RendererInterface::class);
$defaultRenderer->expects(self::never())->method('renderMap');
$renderers = new Renderers(['default' => $defaultRenderer]);
$map = new Map(rendererName: 'unknown');
$renderers->renderMap($map);
}
}

57
tests/TwigTest.php Normal file
View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\UX\Map\Map;
use Symfony\UX\Map\Renderer\RendererInterface;
use Symfony\UX\Map\Tests\Kernel\TwigAppKernel;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
final class TwigTest extends KernelTestCase
{
protected static function getKernelClass(): string
{
return TwigAppKernel::class;
}
public function testRenderMap(): void
{
$map = new Map();
$attributes = ['data-foo' => 'bar'];
$renderer = self::createMock(RendererInterface::class);
$renderer
->expects(self::once())
->method('renderMap')
->with($map, $attributes)
->willReturn('<div data-controller="@symfony/ux-map-foobar"></div>')
;
self::getContainer()->set('test.ux_map.renderers', $renderer);
/** @var \Twig\Environment $twig */
$twig = self::getContainer()->get('twig');
$twig->setLoader(new ChainLoader([
new ArrayLoader([
'test' => '{{ render_map(map, attributes) }}',
]),
$twig->getLoader(),
]));
self::assertSame(
'<div data-controller="@symfony/ux-map-foobar"></div>',
$twig->render('test', ['map' => $map, 'attributes' => $attributes])
);
}
}

82
tests/UXMapBundleTest.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\UX\Map\Renderer\NullRenderer;
use Symfony\UX\Map\Renderer\RendererInterface;
use Symfony\UX\Map\Tests\Kernel\FrameworkAppKernel;
use Symfony\UX\Map\Tests\Kernel\TwigAppKernel;
class UXMapBundleTest extends TestCase
{
/**
* @return iterable<array{0: class-string<Kernel>}>
*/
public static function provideKernelClasses(): iterable
{
yield 'framework' => [FrameworkAppKernel::class];
yield 'twig' => [TwigAppKernel::class];
}
/**
* @dataProvider provideKernelClasses
*
* @param class-string<Kernel> $kernelClass
*/
public function testBootKernel(string $kernelClass): void
{
$kernel = new $kernelClass('test', true);
$kernel->boot();
self::assertArrayHasKey('UXMapBundle', $kernel->getBundles());
}
/**
* @dataProvider provideKernelClasses
*
* @param class-string<Kernel> $kernelClass
*/
public function testNullRendererAsDefault(string $kernelClass): void
{
$expectedRenderer = new NullRenderer(['symfony/ux-map-google', 'symfony/ux-map-leaflet']);
$kernel = new $kernelClass('test', true);
$kernel->boot();
$container = $kernel->getContainer();
$defaultRenderer = $this->extractDefaultRenderer($container);
self::assertEquals($expectedRenderer, $defaultRenderer, 'The default renderer should be a NullRenderer.');
$renderers = $this->extractRenderers($container);
self::assertEquals(['default' => $expectedRenderer], $renderers, 'The renderers should only contain the main renderer, which is a NullRenderer.');
}
private function extractDefaultRenderer(ContainerInterface $container): RendererInterface
{
$renderers = $container->get('test.ux_map.renderers');
return \Closure::bind(fn () => $this->default, $renderers, $renderers::class)();
}
/**
* @return array<string, RendererInterface>
*/
private function extractRenderers(ContainerInterface $container): array
{
$renderers = $container->get('test.ux_map.renderers');
return \Closure::bind(fn () => $this->renderers, $renderers, $renderers::class)();
}
}