multiple provider support

This commit is contained in:
Kacper Donat 2018-09-09 13:05:01 +02:00
parent 432d90fa1b
commit 3e89c654ec
25 changed files with 183 additions and 97 deletions

View File

@ -1,3 +1,4 @@
#index: api_v1:
# path: / resource: ../src/Controller/Api/v1
# controller: App\Controller\DefaultController::index type: annotation
prefix: /{provider}/api/v1

View File

@ -4,7 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed # Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters: parameters:
locale: 'en' locale: 'pl'
services: services:
# default configuration for services in *this* file # default configuration for services in *this* file
@ -15,6 +15,10 @@ services:
# fetching services directly from the container via $container->get() won't work. # fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway. # The best practice is to be explicit about your dependencies anyway.
_instanceof:
App\Provider\Provider:
tags: [ app.provider ]
# makes classes in src/ available to be used as services # makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
@ -60,3 +64,7 @@ services:
App\Service\Normalizer\: App\Service\Normalizer\:
resource: '../src/Service/Normalizer' resource: '../src/Service/Normalizer'
tags: [serializer.normalizer] tags: [serializer.normalizer]
# other servces
App\Service\ProviderResolver:
arguments: [!tagged app.provider]

View File

@ -4,8 +4,6 @@
"author": "Kacper Donat <kadet1090@gmail.com>", "author": "Kacper Donat <kadet1090@gmail.com>",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"webpack": "^4.17.0",
"webpack-cli": "^3.1.0",
"@fortawesome/fontawesome-svg-core": "^1.2.4", "@fortawesome/fontawesome-svg-core": "^1.2.4",
"@fortawesome/pro-light-svg-icons": "^5.3.1", "@fortawesome/pro-light-svg-icons": "^5.3.1",
"@fortawesome/pro-regular-svg-icons": "^5.3.1", "@fortawesome/pro-regular-svg-icons": "^5.3.1",
@ -30,7 +28,12 @@
"vue": "^2.5.17", "vue": "^2.5.17",
"vue-class-component": "^6.2.0", "vue-class-component": "^6.2.0",
"vue-property-decorator": "^7.0.0", "vue-property-decorator": "^7.0.0",
"webpack": "^4.17.0",
"webpack-cli": "^3.1.0",
"xmldom": "^0.1.27", "xmldom": "^0.1.27",
"xpath": "^0.0.27" "xpath": "^0.0.27"
},
"dependencies": {
"mini-css-extract-plugin": "^0.4.2"
} }
} }

View File

@ -24,6 +24,10 @@
{{ departure.line.symbol }} {{ departure.line.symbol }}
</div> </div>
<div class="line__display">{{ departure.display }}</div> <div class="line__display">{{ departure.display }}</div>
<div class="line__perks flex-space-left">
<fa :icon="['fas', 'walking']" fixed-width v-if="departure.line.fast"/>
<fa :icon="['fal', 'moon']" fixed-width v-if="departure.line.night"/>
</div>
</div> </div>
<div class="departure__time"> <div class="departure__time">

View File

@ -34,9 +34,11 @@
</div> </div>
</div> </div>
<div class="alert alert-warning" v-else-if="filter.length > 2"> <div class="alert alert-warning" v-else-if="filter.length > 2">
<fa :icon="['far', 'exclamation-triangle']"></fa>
Nie znaleziono więcej przystanków, spełniających te kryteria. Nie znaleziono więcej przystanków, spełniających te kryteria.
</div> </div>
<div class="alert alert-info" v-else> <div class="alert alert-info" v-else>
<fa :icon="['far', 'search']"></fa>
Wprowadź zapytanie powyżej, aby wyszukać przystanek. Wprowadź zapytanie powyżej, aby wyszukać przystanek.
</div> </div>
</div> </div>

View File

@ -13,3 +13,19 @@
.flex-space-right { .flex-space-right {
margin-right: auto; margin-right: auto;
} }
.alert {
border-width: 0;
background: transparent;
@each $color, $value in $theme-colors {
&.alert-#{$color} {
border-bottom: 2px solid theme-color-level($color, $alert-color-level);
transition: background-color ease 200ms;
&:hover {
background-color: rgba(theme-color-level($color, $alert-bg-level), .5);
}
}
}
}

View File

@ -1,4 +1,4 @@
$border-radius: 2px; $border-radius: 0;
$border-radius-lg: $border-radius; $border-radius-lg: $border-radius;
$border-radius-sm: $border-radius; $border-radius-sm: $border-radius;

View File

@ -23,7 +23,6 @@ export class Departures extends Vue {
async update() { async update() {
const response = await fetch(urls.prepare(urls.departures, { const response = await fetch(urls.prepare(urls.departures, {
stop: this.stops.map(stop => stop.id), stop: this.stops.map(stop => stop.id),
provider: 'gdansk'
})); }));
if (response.ok) { if (response.ok) {

View File

@ -9,26 +9,11 @@ import stop = require('../../components/stop.html');
import { Prop, Watch } from "vue-property-decorator"; import { Prop, Watch } from "vue-property-decorator";
import { filter, map } from "../utils"; import { filter, map } from "../utils";
import { debounce, throttle } from "../decorators"; import { debounce } from "../decorators";
import { Departures } from "./departures";
@Component({ template: picker }) @Component({ template: picker })
export class PickerComponent extends Vue { export class PickerComponent extends Vue {
protected stops?: Stop[] = [{ protected stops?: Stop[] = [];
"id": 2001,
"name": "Dworzec Główny",
"description": null,
"location": [54.35544, 18.64565],
"variant": "01",
"onDemand": false
}, {
"id": 2002,
"name": "Dworzec Główny",
"description": null,
"location": [54.35541, 18.64548],
"variant": "02",
"onDemand": false
}];
private remove(stop: Stop) { private remove(stop: Stop) {
this.stops = this.stops.filter(s => s != stop); this.stops = this.stops.filter(s => s != stop);
@ -70,10 +55,7 @@ export class FinderComponent extends Vue {
this.state = 'fetching'; this.state = 'fetching';
const response = await fetch(urls.prepare(urls.stops.search, { const response = await fetch(urls.prepare(urls.stops.search, { name: this.filter }));
name: this.filter,
provider: 'gdansk'
}));
if (response.ok) { if (response.ok) {
this.found = await response.json(); this.found = await response.json();

View File

@ -45,12 +45,14 @@ export function prepare(url: string, params: UrlParams = { }) {
return Object.keys(params).length > 0 ? `${url}?${query(params)}` : url; return Object.keys(params).length > 0 ? `${url}?${query(params)}` : url;
} }
const base = '/{provider}/api/v1';
export default { export default {
departures: '/{provider}/departures', departures: `${base}/departures`,
stops: { stops: {
all: '/{provider}/stops', all: `${base}/stops`,
search: '/{provider}/stops/search', search: `${base}/stops/search`,
get: '/{provider}/stop/{id}' get: `${base}/stops/{id}`
}, },
prepare prepare: (url: string, params: UrlParams = { }) => prepare(url, Object.assign({}, { provider: window['app'].provider }, params))
} }

View File

@ -1,8 +1,9 @@
<?php <?php
namespace App\Controller; namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Departure; use App\Model\Departure;
use App\Provider\DepartureRepository; use App\Provider\DepartureRepository;
use App\Provider\StopRepository; use App\Provider\StopRepository;
@ -12,7 +13,7 @@ use Symfony\Component\Routing\Annotation\Route;
/** /**
* Class DeparturesController * Class DeparturesController
* *
* @Route("/{provider}/departures") * @Route("/departures")
*/ */
class DeparturesController extends Controller class DeparturesController extends Controller
{ {

View File

@ -1,13 +1,14 @@
<?php <?php
namespace App\Controller; namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Provider\MessageRepository; use App\Provider\MessageRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
/** /**
* @Route("/{provider}/messages") * @Route("/messages")
*/ */
class MessagesController extends Controller class MessagesController extends Controller
{ {

View File

@ -1,8 +1,9 @@
<?php <?php
namespace App\Controller; namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Provider\StopRepository; use App\Provider\StopRepository;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@ -11,7 +12,7 @@ use Symfony\Component\Routing\Annotation\Route;
* Class StopsController * Class StopsController
* *
* @package App\Controller * @package App\Controller
* @Route("/{provider}/stops") * @Route("/stops")
*/ */
class StopsController extends Controller class StopsController extends Controller
{ {

View File

@ -1,17 +0,0 @@
<?php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
class HomepageController extends Controller
{
/**
* @Route("/", name="home")
*/
public function homepage()
{
return $this->render('base.html.twig');
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Controller;
use App\Provider\Provider;
use App\Service\ProviderResolver;
use Symfony\Component\Routing\Annotation\Route;
class MainController extends Controller
{
/**
* @Route("/", name="choose")
*/
public function choose(ProviderResolver $resolver)
{
return $this->render('choose.html.twig', ['providers' => $resolver->all()]);
}
/**
* @Route("/{provider}", name="app")
*/
public function app(Provider $provider)
{
return $this->render('app.html.twig', ['provider' => $provider]);
}
}

View File

@ -8,4 +8,7 @@ interface Provider
public function getLineRepository(): LineRepository; public function getLineRepository(): LineRepository;
public function getStopRepository(): StopRepository; public function getStopRepository(): StopRepository;
public function getMessageRepository(): MessageRepository; public function getMessageRepository(): MessageRepository;
public function getName();
public function getIdentifier();
} }

View File

@ -27,6 +27,7 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
private $em; private $em;
private $ids; private $ids;
private $logger; private $logger;
private $provider;
/** /**
* ZtmGdanskDataUpdateSubscriber constructor. * ZtmGdanskDataUpdateSubscriber constructor.
@ -34,19 +35,24 @@ class ZtmGdanskDataUpdateSubscriber implements EventSubscriberInterface
* @param $provider * @param $provider
* @param $em * @param $em
*/ */
public function __construct(EntityManagerInterface $em, IdUtils $ids, LoggerInterface $logger) public function __construct(
{ EntityManagerInterface $em,
IdUtils $ids,
LoggerInterface $logger,
ZtmGdanskProvider $provider
) {
$this->em = $em; $this->em = $em;
$this->ids = $ids; $this->ids = $ids;
$this->logger = $logger; $this->logger = $logger;
$this->provider = $provider;
} }
public function update() public function update()
{ {
$provider = ProviderEntity::createFromArray([ $provider = ProviderEntity::createFromArray([
'name' => 'ZTM Gdańsk', 'name' => $this->provider->getName(),
'class' => ZtmGdanskProvider::class, 'class' => ZtmGdanskProvider::class,
'id' => 'ztm-gdansk', 'id' => $this->provider->getIdentifier(),
]); ]);
$this->em->persist($provider); $this->em->persist($provider);

View File

@ -16,13 +16,23 @@ class ZtmGdanskProvider implements Provider
private $stops; private $stops;
private $messages; private $messages;
public function getName()
{
return 'MZKZG Trójmiasto';
}
public function getIdentifier()
{
return 'trojmiasto';
}
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
GenericLineRepository $lines, GenericLineRepository $lines,
GenericStopRepository $stops, GenericStopRepository $stops,
ZtmGdanskMessageRepository $messages ZtmGdanskMessageRepository $messages
) { ) {
$provider = $em->getReference(ProviderEntity::class, 'ztm-gdansk'); $provider = $em->getReference(ProviderEntity::class, $this->getIdentifier());
$lines = $lines->withProvider($provider); $lines = $lines->withProvider($provider);
$stops = $stops->withProvider($provider); $stops = $stops->withProvider($provider);

View File

@ -64,6 +64,8 @@ final class EntityConverter
'symbol' => $entity->getSymbol(), 'symbol' => $entity->getSymbol(),
'type' => $entity->getType(), 'type' => $entity->getType(),
'operator' => $convert($entity->getOperator()), 'operator' => $convert($entity->getOperator()),
'night' => $entity->isNight(),
'fast' => $entity->isFast(),
'tracks' => $this->collection($entity->getTracks(), $convert), 'tracks' => $this->collection($entity->getTracks(), $convert),
]); ]);
break; break;

View File

@ -3,30 +3,18 @@
namespace App\Service; namespace App\Service;
use App\Exception\NonExistentServiceException; use App\Exception\NonExistentServiceException;
use App\Provider\Provider; use App\Provider\Provider;
use App\Provider\ZtmGdanskProvider; use Kadet\Functional\Transforms as t;
use Tightenco\Collect\Support\Collection;
class ProviderResolver class ProviderResolver
{ {
private const PROVIDER = [ private $providers;
'gdansk' => ZtmGdanskProvider::class
];
/** public function __construct($providers)
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
private $container;
/**
* ProviderResolver constructor.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container)
{ {
$this->container = $container; $this->providers = collect($providers)->keyBy(t\property('identifier'));
} }
/**\ /**\
@ -37,10 +25,17 @@ class ProviderResolver
*/ */
public function resolve(string $name): Provider public function resolve(string $name): Provider
{ {
if (!array_key_exists($name, static::PROVIDER)) { if (!$this->providers->has($name)) {
throw new NonExistentServiceException(); $message = sprintf("Provider '%s' doesn't exist, you can choose from: %s", $name, $this->providers->keys()->implode(', '));
throw new NonExistentServiceException($message);
} }
return $this->container->get(static::PROVIDER[$name]); return $this->providers->get($name);
}
/** @return Provider[] */
public function all(): Collection
{
return clone $this->providers;
} }
} }

13
templates/app.html.twig Normal file
View File

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block title "#{parent()} - #{provider.name}" %}
{% block body %}
<stop-picker></stop-picker>
{% endblock %}
{% block javascripts %}
<script>
window.app = {
provider: {{ provider.identifier|json_encode|raw }}
}
</script>
{% endblock %}

View File

@ -1,16 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8"/>
<title>{% block title %}Welcome!{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block stylesheets %}{% endblock %} <link rel="stylesheet" href="main.css" />
<title>{% block title %}Czy dojadę?{% endblock %}</title>
</head> </head>
<body> <body>
{% block body %} <main role="main" class="container" id="app">
<main id="app" class="container"> {% block body %}{% endblock %}
<stop-picker></stop-picker>
</main> </main>
{% endblock %}
<script src="bundle.js"></script> <script src="bundle.js"></script>
{% block javascripts %}{% endblock %} {% block javascripts %}{% endblock %}
</body> </body>

View File

@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}
{% block body %}
<div class="alert alert-primary">
<fa :icon="['fal', 'info-circle']"></fa>
Wybierz źródło danych
</div>
{% for provider in providers %}
<a href="{{ path('app', { provider: provider.identifier }) }}">{{ provider.name }}</a>
{% endfor %}
{% endblock %}

View File

@ -1,4 +1,6 @@
const path = require('path'); const path = require('path');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const config = { const config = {
entry: { entry: {
main: ['./resources/ts/app.ts'], main: ['./resources/ts/app.ts'],
@ -24,7 +26,9 @@ const config = {
}] }]
},{ },{
test: /\.s[ac]ss$/, test: /\.s[ac]ss$/,
use: ["style-loader", "css-loader?sourceMap", "sass-loader?sourceMap"] use: [{
loader: MiniCssExtractPlugin.loader,
}, "css-loader?sourceMap", "sass-loader?sourceMap"]
}, { }, {
test: /\.css$/, test: /\.css$/,
use: ["style-loader", "css-loader"] use: ["style-loader", "css-loader"]
@ -46,6 +50,9 @@ const config = {
use: 'raw-loader' use: 'raw-loader'
}] }]
}, },
plugins: [
new MiniCssExtractPlugin({ filename: '[name].css' })
],
}; };
module.exports = (env, argv) => { module.exports = (env, argv) => {

View File

@ -1967,6 +1967,14 @@ mimic-fn@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
mini-css-extract-plugin@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.2.tgz#b3ecc0d6b1bbe5ff14add42b946a7b200cf78651"
dependencies:
loader-utils "^1.1.0"
schema-utils "^1.0.0"
webpack-sources "^1.1.0"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"