mirror of
https://github.com/symfony/ux-map.git
synced 2026-03-23 23:42:07 +01:00
[Map] Create Map component
This commit is contained in:
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal 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
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
vendor
|
||||
composer.lock
|
||||
.phpunit.result.cache
|
||||
3
.symfony.bundle.yaml
Normal file
3
.symfony.bundle.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
branches: ["2.x"]
|
||||
maintained_branches: ["2.x"]
|
||||
doc_dir: "doc"
|
||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Component added
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal 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
16
README.md
Normal 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)
|
||||
55
assets/dist/abstract_map_controller.d.ts
vendored
Normal file
55
assets/dist/abstract_map_controller.d.ts
vendored
Normal 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
47
assets/dist/abstract_map_controller.js
vendored
Normal 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
21
assets/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
121
assets/src/abstract_map_controller.ts
Normal file
121
assets/src/abstract_map_controller.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
76
assets/test/abstract_map_controller.test.ts
Normal file
76
assets/test/abstract_map_controller.test.ts
Normal 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: 700px; margin: 10px"
|
||||
data-map-provider-options-value="{}"
|
||||
data-map-view-value="{"center":{"lat":48.8566,"lng":2.3522},"zoom":4,"fitBoundsToMarkers":true,"options":{},"markers":[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null},{"position":{"lat":45.764,"lng":4.8357},"title":"Lyon","infoWindow":{"headerContent":"<b>Lyon<\/b>","content":"The French town in the historic Rh\u00f4ne-Alpes region, located at the junction of the Rh\u00f4ne and Sa\u00f4ne rivers.","position":null,"opened":false,"autoClose":true}}]}"
|
||||
></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
51
composer.json
Normal 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
45
config/services.php
Normal 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
207
doc/index.rst
Normal 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
26
phpunit.xml.dist
Normal 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&max[direct]=0"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
19
src/Exception/Exception.php
Normal file
19
src/Exception/Exception.php
Normal 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
|
||||
{
|
||||
}
|
||||
19
src/Exception/IncompleteDsnException.php
Normal file
19
src/Exception/IncompleteDsnException.php
Normal 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
|
||||
{
|
||||
}
|
||||
19
src/Exception/InvalidArgumentException.php
Normal file
19
src/Exception/InvalidArgumentException.php
Normal 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
|
||||
{
|
||||
}
|
||||
19
src/Exception/LogicException.php
Normal file
19
src/Exception/LogicException.php
Normal 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
|
||||
{
|
||||
}
|
||||
19
src/Exception/RuntimeException.php
Normal file
19
src/Exception/RuntimeException.php
Normal 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
|
||||
{
|
||||
}
|
||||
38
src/Exception/UnsupportedSchemeException.php
Normal file
38
src/Exception/UnsupportedSchemeException.php
Normal 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
40
src/InfoWindow.php
Normal 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
104
src/Map.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
src/MapOptionsInterface.php
Normal file
23
src/MapOptionsInterface.php
Normal 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
36
src/Marker.php
Normal 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
46
src/Point.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
65
src/Renderer/AbstractRenderer.php
Normal file
65
src/Renderer/AbstractRenderer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
41
src/Renderer/AbstractRendererFactory.php
Normal file
41
src/Renderer/AbstractRendererFactory.php
Normal 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
81
src/Renderer/Dsn.php
Normal 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;
|
||||
}
|
||||
}
|
||||
43
src/Renderer/NullRenderer.php
Normal file
43
src/Renderer/NullRenderer.php
Normal 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';
|
||||
}
|
||||
}
|
||||
39
src/Renderer/NullRendererFactory.php
Normal file
39
src/Renderer/NullRendererFactory.php
Normal 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
56
src/Renderer/Renderer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
22
src/Renderer/RendererFactoryInterface.php
Normal file
22
src/Renderer/RendererFactoryInterface.php
Normal 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;
|
||||
}
|
||||
25
src/Renderer/RendererInterface.php
Normal file
25
src/Renderer/RendererInterface.php
Normal 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;
|
||||
}
|
||||
64
src/Renderer/Renderers.php
Normal file
64
src/Renderer/Renderers.php
Normal 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));
|
||||
}
|
||||
}
|
||||
92
src/Test/RendererFactoryTestCase.php
Normal file
92
src/Test/RendererFactoryTestCase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
35
src/Test/RendererTestCase.php
Normal file
35
src/Test/RendererTestCase.php
Normal 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
29
src/Twig/MapExtension.php
Normal 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
109
src/UXMapBundle.php
Normal 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
41
tests/InfoWindowTest.php
Normal 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());
|
||||
}
|
||||
}
|
||||
41
tests/Kernel/AppKernelTrait.php
Normal file
41
tests/Kernel/AppKernelTrait.php
Normal 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;
|
||||
}
|
||||
}
|
||||
44
tests/Kernel/FrameworkAppKernel.php
Normal file
44
tests/Kernel/FrameworkAppKernel.php
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
46
tests/Kernel/TwigAppKernel.php
Normal file
46
tests/Kernel/TwigAppKernel.php
Normal 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
121
tests/MapTest.php
Normal 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
55
tests/MarkerTest.php
Normal 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
45
tests/PointTest.php
Normal 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
103
tests/Renderer/DsnTest.php
Normal 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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
47
tests/Renderer/NullRendererFactoryTest.php
Normal file
47
tests/Renderer/NullRendererFactoryTest.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
54
tests/Renderer/NullRendererTest.php
Normal file
54
tests/Renderer/NullRendererTest.php
Normal 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(), []);
|
||||
}
|
||||
}
|
||||
46
tests/Renderer/RendererTest.php
Normal file
46
tests/Renderer/RendererTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
71
tests/Renderer/RenderersTest.php
Normal file
71
tests/Renderer/RenderersTest.php
Normal 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
57
tests/TwigTest.php
Normal 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
82
tests/UXMapBundleTest.php
Normal 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)();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user