Compare commits

..

No commits in common. "master" and "0.2" have entirely different histories.
master ... 0.2

410 changed files with 5632 additions and 14635 deletions

View File

@ -1 +0,0 @@
PHP_IDE_CONFIG=serverName=cojedzie

View File

@ -1,31 +0,0 @@
server {
root /var/www/front/public/;
server_name cojedzie.localhost;
location /_profiler/ {
try_files $uri $uri/ @api;
}
location /bundles/ {
try_files $uri $uri/ @api;
}
location /api/ {
try_files $uri $uri/ @api;
}
location / {
try_files $uri $uri/ @frontend;
}
location @frontend {
proxy_pass http://frontend:3000;
proxy_intercept_errors on;
}
location @api {
proxy_pass http://api:8080;
proxy_intercept_errors on;
}
}

View File

@ -1,44 +0,0 @@
server {
root /var/www/front/public/;
server_name cojedzie.localhost;
location /api/ {
root /var/www/api/public/;
try_files $uri $uri/ index.php$is_args$args;
}
location /_profiler/ {
root /var/www/api/public/;
try_files $uri $uri/ index.php$is_args$args;
}
location /bundles/ {
root /var/www/api/public/;
try_files $uri $uri/;
}
location / {
try_files $uri $uri/ @frontend;
}
location @frontend {
proxy_pass http://frontend:3000;
proxy_intercept_errors on;
}
location ~ (.+).php(/|$) {
root /var/www/api/public/;
fastcgi_pass api:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/public/$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/public/;
fastcgi_param APP_ENV "dev";
fastcgi_param DATABASE_URL "sqlite:///%kernel.project_dir%/var/app.db";
internal;
}
}

17
.env.dist Normal file
View File

@ -0,0 +1,17 @@
# This file is a "template" of which env vars need to be defined for your application
# Copy this file to .env file for development, create environment variables when deploying to production
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
GOOGLE_ANALYTICS=
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=1bdf86cdc78fba654e4f2c309c6bbdbd
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# Configure your db driver and server_version in config/packages/doctrine.yaml
DATABASE_URL=sqlite:///%kernel.project_dir%/var/app.db
###< doctrine/doctrine-bundle ###

16
.gitignore vendored
View File

@ -1 +1,17 @@
###> symfony/framework-bundle ###
/.env
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> symfony/web-server-bundle ###
/.web-server-pid
###< symfony/web-server-bundle ###
/node_modules/
/.idea/
/public/*
!/public/index.php
!/public/manifest.json

71
CLA.md
View File

@ -1,71 +0,0 @@
# Co Jedzie Individual Contributor License Agreement
Adapted from http://www.apache.org/licenses/icla.txt © The Apache Software Foundation
Thank you for your interest in Co Jedzie (the **"Project"**) by Kacper Donat (the **"Author"**). In order to clarify the
intellectual property license granted with Contributions from any person or entity, the Author must have a Contributor
License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms
below. This license is for your protection as a Contributor as well as the protection of the Author and its users; it
does not change your rights to use your own Contributions for any other purpose.
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the
Author. In return, the Author shall not use Your Contributions in a way that is contrary to the public benefit or
inconsistent with its bylaws in effect at the time of the Contribution. Except for the license granted herein to the
Author and recipients of software distributed by the Author, You reserve all right, title, and interest in and to Your
Contributions.
1. Definitions.
- **"You"** (or **"Your"**) shall mean the copyright owner or legal entity authorized by the copyright owner that is
making this Agreement with the Author. For legal entities, the entity making a Contribution and all other entities
that control, are controlled by, or are under common control with that entity are considered to be a single
Contributor. For the purposes of this definition, **"control"** means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or
more of the outstanding shares, or (iii) beneficial ownership of such entity.
- **"Contribution"** shall mean any original work of authorship, including any modifications or additions to an existing
work, that is intentionally submitted by You to the Author for inclusion in, or documentation of, any of the products
owned or managed by the Author (the **"Work"**). For the purposes of this definition, **"submitted"** means any form
of electronic, verbal, or written communication sent to the Author or its representatives, including but not limited
to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed
by, or on behalf of, the Author for the purpose of discussing and improving the Work, but excluding communication that
is conspicuously marked or otherwise designated in writing by You as **"Not a Contribution."**
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to the Author and
to recipients of software distributed by the Author a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform,
sublicense, re-license, and distribute Your Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to the Author and to
recipients of software distributed by the Author a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import,
and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are
necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which
such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (
including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity
under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to
intellectual property that you create that includes your Contributions, you represent that you have received
permission to make Contributions on behalf of that employer, that your employer has waived such rights for your
Contributions to the Author, or that your employer has executed a separate Corporate CLA with the Author.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of
others). You represent that Your Contribution submissions include complete details of any third-party license or
other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware
and which are associated with any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support.
You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may submit it to the Author separately from
any Contribution, identifying the complete details of its source and of any license or other restriction (including,
but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and
conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
8. You agree to notify the Author of any facts or circumstances of which you become aware that would make these
representations inaccurate in any respect.

View File

@ -1,38 +0,0 @@
# How to contribute?
Thanks for your interest in the project!
## I'd like to propose some feature / change...
Cool! Go ahead, [create an issue] and describe your proposal so anyone can see it. You can also vote on features that
you want the most.
## I've found a bug!
Well, less cool! Before creating an issue, please check if the bug remains after hard refreshing (usually `ctrl+F5`) the
application. If the answer is yes, or this is not the first encounter of it please [create an issue] and describe the
problem. If you can, please attach screenshots (especially if this is visual bug), and console logs (especially for
connection problems) - this will help to reproduce the problem.
## I've got some spare resources on my server...
Soon you will be able to help the project by hosting your own API node that will be available for clients to use.
More details to come soon.
## I want to contribute some code...
That's great! If you want to make changes to API (which is responsible for collecting and supplying applicaiton with
data) please check the [API contribution guidelines], if you are interested in UI side of the app please read the
[frontend contribution guidelines].
### Contributor License Agreement
Unfortunately due to this project nature and license I need you to sign [Contributor License Agreement] - the nice thing
is that it can be done with simple push of a button! **You still will have full copyright to your contribution** but
basically you consent that you are entitled to code you are submitting and also to allow me to license this project on
other terms if needed to, for example, local governments. If you don't want to sign - I understand - but I won't be able
to accept your contribution :(
[Contributor License Agreement]: ./CLA.md
[create an issue]: https://github.com/cojedzie/cojedzie/issues/new
[API contribution guidelines]: ./api/CONTRIBUTING.md
[frontend contribution guidelines]: ./front/CONTRIBUTING.md

View File

@ -1,41 +0,0 @@
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the License, as defined
below, subject to the following condition.
Without limiting other conditions in the License, the grant of rights under the
License will not include, and the License does not grant to you, the right to
Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or all of the rights
granted to you under the License to provide to third parties, for a fee or other
consideration (including without limitation fees for hosting or consulting/
support services related to the Software), a product or service whose value
derives, entirely or substantially, from the functionality of the Software. Any
license notice or attribution required by the License must also include this
Commons Clause License Condition notice.
Software: Co Jedzie
License: MIT
Licensor: Kacper Donat
Copyright 2020 Kacper Donat
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.

View File

@ -1,32 +0,0 @@
# [Co Jedzie](https://cojedzie.pl)
![Co Jedzie logo](https://i.imgur.com/oMCMAxa.png)
![screenshot](https://i.imgur.com/kpcwgjb.png)
Co Jedzie is an app that allows you to quickly and easily check realtime departure times on public transport stops. It
aims to be the central hub for all public transport information you will need.
You can use the app at [cojedzie.pl](https://cojedzie.pl).
## Roadmap
Co Jedzie is in active development, roadmap of the project can be found on [trello]. This roadmap is regularly updated
and represents current state of the project. Feel free to take a look.
### Contributing to roadmap
If you have found a bug or want to propose some changes feel free to create an [issue] explaining your proposal or
problem. Issue management and discussion would be done on the github, but planning will be carried away to the [trello]
trello with linked issue.
## Contributing
Wan't to contribute? Nice! Please see [CONTRIBUTING.md]
## License
This project is [fair-code](https://faircode.io/) licensed under [MIT with Commons Clause](./LICENSE.md). Basically, Co
Jedzie is free and code is available to everyone, but it's not allowed to make money directly with it without
authors permission.
Note that data collected from available data sources is licensed by their respective owners, thus it may be
available under different terms than the project itself and may require additional permissions to use.
[trello]: https://trello.com/b/QXqDvmoG/co-jedzie
[issue]: https://github.com/cojedzie/cojedzie/issues/new
[CONTRIBUTING.md]: ./CONTRIBUTING.md

View File

@ -1,2 +0,0 @@
/vendor/
/var/

View File

@ -1,22 +0,0 @@
# This file is a "template" of which env vars need to be defined for your application
# Copy this file to .env file for development, create environment variables when deploying to production
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=494a9d1f8cc383f16075f4d5e54ae1a2
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
DATABASE_URL="sqlite:///%kernel.project_dir%/var/app.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
APP_EVENT_QUEUE_DSN="doctrine://default"
###< symfony/messenger ###

16
api/.gitignore vendored
View File

@ -1,16 +0,0 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
/node_modules/
/public/*
!/public/index.php
###> baldinof/roadrunner-bundle ###
/bin/rr
###< baldinof/roadrunner-bundle ###

View File

@ -1,11 +0,0 @@
include:
- .rr.yaml
reload:
enabled: true
interval: 1s
patterns: [".php"]
services:
http:
dirs: ["."]
recursive: true

View File

@ -1,13 +0,0 @@
http:
address: "0.0.0.0:8080"
uploads:
forbid: [".php", ".exe", ".bat"]
workers:
command: "php bin/console baldinof:roadrunner:worker"
relay: "unix://var/roadrunner.sock"
static:
dir: "public"
forbid: [".php", ".htaccess"]

View File

@ -1,3 +0,0 @@
# Contributing guidelines
TBD

View File

@ -1,28 +0,0 @@
FROM php:7.4-fpm-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN install-php-extensions bcmath intl opcache zip sockets xdebug-^3.0;
RUN apk add git;
# XDebug
RUN echo "xdebug.mode=debug" >> $PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini && \
echo "xdebug.discover_client_host=On" >> $PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini;
# Blackfire
RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \
&& curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/linux/amd64/$version \
&& mkdir -p /tmp/blackfire \
&& tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \
&& mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \
&& printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" > $PHP_INI_DIR/conf.d/blackfire.ini \
&& rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz
# Timezone
RUN ln -snf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime && \
echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini;
WORKDIR /var/www
EXPOSE 9001

View File

@ -1,3 +0,0 @@
#!/bin/sh
exec "$@"

View File

@ -1,5 +0,0 @@
#!/bin/sh
./bin/console doctrine:migrations:migrate --no-interaction
exec "$@"

View File

@ -1,23 +0,0 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__).'/vendor/autoload.php';
if (!class_exists(Dotenv::class)) {
throw new LogicException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
}
// Load cached env vars if the .env.local.php file exists
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
(new Dotenv(false))->populate($env);
} else {
// load all the .env files
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
}
$_SERVER += $_ENV;
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';

View File

@ -1,18 +0,0 @@
baldinof_road_runner:
# The kernel is preserved between requests. Change this to `true`
# if you want to reboot it, and use a fresh container on each request.
should_reboot_kernel: false
# Integrations are automatically detected, depending on installed bundle & current configuration
# See https://github.com/baldinof/roadrunner-bundle#integrations
default_integrations: true
# Allow to send prometheus metrics to the master RoadRunner process,
# via a `Spiral\RoadRunner\MetricsInterface` service.
# See https://github.com/baldinof/roadrunner-bundle#metrics
metrics_enabled: false
# You can use middlewares to manipulate PSR requests & responses.
# See https://github.com/baldinof/roadrunner-bundle#middlewares
# middlewares:
# - App\Middleware\YourMiddleware

View File

@ -1,19 +0,0 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

View File

@ -1,3 +0,0 @@
doctrine_migrations:
migrations_paths:
'DoctrineMigrations': '%kernel.project_dir%/migrations'

View File

@ -1,6 +0,0 @@
framework:
secret: '%env(APP_SECRET)%'
csrf_protection: false
php_errors:
log: true

View File

@ -1,11 +0,0 @@
parameters:
env(APP_EVENT_QUEUE): "doctrine://default"
framework:
messenger:
transports:
main: '%env(resolve:APP_EVENT_QUEUE)%'
sync: 'sync://'
routing:
'App\Message\UpdateDataMessage': main

View File

@ -1,20 +0,0 @@
doctrine:
orm:
auto_generate_proxy_classes: false
metadata_cache_driver:
type: pool
pool: doctrine.system_cache_pool
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@ -1,2 +0,0 @@
twig:
strict_variables: true

View File

@ -1,5 +0,0 @@
twig:
default_path: '%kernel.project_dir%/templates'
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
exception_controller: null

View File

@ -1,9 +0,0 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/srcApp_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/srcApp_KernelProdContainer.preload.php';
}
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

View File

@ -1,16 +0,0 @@
api_v1:
resource: ../src/Controller/Api/v1
type: annotation
prefix: /api/v1/{provider}
api_v1_providers:
path: /api/v1/providers
methods: ["GET"]
defaults:
_controller: '\App\Controller\Api\v1\ProviderController::index'
api_v1_providers_one:
path: /api/v1/providers/{id}
methods: ["GET"]
defaults:
_controller: '\App\Controller\Api\v1\ProviderController::one'

View File

@ -1,3 +0,0 @@
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200131151757 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('CREATE TEMPORARY TABLE __temp__stop AS SELECT id, provider_id, name, description, variant, latitude, longitude, on_demand FROM stop');
$this->addSql('DROP TABLE stop');
$this->addSql('CREATE TABLE stop (id VARCHAR(255) NOT NULL COLLATE BINARY, provider_id VARCHAR(255) DEFAULT NULL COLLATE BINARY, name VARCHAR(255) NOT NULL COLLATE BINARY, description VARCHAR(255) DEFAULT NULL COLLATE BINARY, variant VARCHAR(255) DEFAULT NULL COLLATE BINARY, latitude DOUBLE PRECISION DEFAULT NULL, longitude DOUBLE PRECISION DEFAULT NULL, on_demand BOOLEAN NOT NULL, group_name VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO stop (id, provider_id, name, description, variant, latitude, longitude, on_demand) SELECT id, provider_id, name, description, variant, latitude, longitude, on_demand FROM __temp__stop');
$this->addSql('DROP TABLE __temp__stop');
$this->addSql('CREATE INDEX group_idx ON stop (group_name)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('DROP INDEX group_idx');
$this->addSql('CREATE TEMPORARY TABLE __temp__stop AS SELECT id, name, description, variant, latitude, longitude, on_demand, provider_id FROM stop');
$this->addSql('DROP TABLE stop');
$this->addSql('CREATE TABLE stop (id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, variant VARCHAR(255) DEFAULT NULL, latitude DOUBLE PRECISION DEFAULT NULL, longitude DOUBLE PRECISION DEFAULT NULL, on_demand BOOLEAN NOT NULL, provider_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO stop (id, name, description, variant, latitude, longitude, on_demand, provider_id) SELECT id, name, description, variant, latitude, longitude, on_demand, provider_id FROM __temp__stop');
$this->addSql('DROP TABLE __temp__stop');
}
}

View File

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200206183956 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('CREATE TEMPORARY TABLE __temp__track AS SELECT id, line_id, provider_id, variant, description FROM track');
$this->addSql('DROP TABLE track');
$this->addSql('CREATE TABLE track (id VARCHAR(255) NOT NULL COLLATE BINARY, line_id VARCHAR(255) DEFAULT NULL COLLATE BINARY, provider_id VARCHAR(255) DEFAULT NULL COLLATE BINARY, variant VARCHAR(16) DEFAULT NULL COLLATE BINARY, description VARCHAR(256) DEFAULT NULL COLLATE BINARY, final_id INTEGER DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO track (id, line_id, provider_id, variant, description) SELECT id, line_id, provider_id, variant, description FROM __temp__track');
$this->addSql('DROP TABLE __temp__track');
$this->addSql('CREATE UNIQUE INDEX UNIQ_D6E3F8A613D41B2D ON track (final_id)');
$this->addSql('CREATE TEMPORARY TABLE __temp__track_stop AS SELECT stop_id, track_id, sequence FROM track_stop');
$this->addSql('DROP TABLE track_stop');
$this->addSql('CREATE TABLE track_stop (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, sequence INTEGER NOT NULL, stop_id VARCHAR(255) DEFAULT NULL, track_id VARCHAR(255) DEFAULT NULL)');
$this->addSql('INSERT INTO track_stop (stop_id, track_id, sequence) SELECT stop_id, track_id, sequence FROM __temp__track_stop');
$this->addSql('DROP TABLE __temp__track_stop');
$this->addSql('CREATE UNIQUE INDEX stop_in_track_idx ON track_stop (stop_id, track_id, sequence)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('DROP INDEX UNIQ_D6E3F8A613D41B2D');
$this->addSql('CREATE TEMPORARY TABLE __temp__track AS SELECT id, variant, description, line_id, provider_id FROM track');
$this->addSql('DROP TABLE track');
$this->addSql('CREATE TABLE track (id VARCHAR(255) NOT NULL, variant VARCHAR(16) DEFAULT NULL, description VARCHAR(256) DEFAULT NULL, line_id VARCHAR(255) DEFAULT NULL, provider_id VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO track (id, variant, description, line_id, provider_id) SELECT id, variant, description, line_id, provider_id FROM __temp__track');
$this->addSql('DROP TABLE __temp__track');
$this->addSql('DROP INDEX stop_in_track_idx');
$this->addSql('CREATE TEMPORARY TABLE __temp__track_stop AS SELECT sequence, stop_id, track_id FROM track_stop');
$this->addSql('DROP TABLE track_stop');
$this->addSql('CREATE TABLE track_stop (sequence INTEGER NOT NULL, stop_id VARCHAR(255) NOT NULL COLLATE BINARY, track_id VARCHAR(255) NOT NULL COLLATE BINARY, PRIMARY KEY(stop_id, track_id, sequence))');
$this->addSql('INSERT INTO track_stop (sequence, stop_id, track_id) SELECT sequence, stop_id, track_id FROM __temp__track_stop');
$this->addSql('DROP TABLE __temp__track_stop');
}
}

View File

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200314112552 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('CREATE TEMPORARY TABLE __temp__trip_stop AS SELECT stop_id, trip_id, sequence, arrival, departure FROM trip_stop');
$this->addSql('DROP TABLE trip_stop');
$this->addSql('CREATE TABLE trip_stop (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, sequence INTEGER NOT NULL, arrival DATETIME NOT NULL, departure DATETIME NOT NULL, stop_id VARCHAR(255) DEFAULT NULL, trip_id VARCHAR(255) DEFAULT NULL)');
$this->addSql('INSERT INTO trip_stop (stop_id, trip_id, sequence, arrival, departure) SELECT stop_id, trip_id, sequence, arrival, departure FROM __temp__trip_stop');
$this->addSql('DROP TABLE __temp__trip_stop');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.');
$this->addSql('CREATE TEMPORARY TABLE __temp__trip_stop AS SELECT sequence, arrival, departure, stop_id, trip_id FROM trip_stop');
$this->addSql('DROP TABLE trip_stop');
$this->addSql('CREATE TABLE trip_stop (sequence INTEGER NOT NULL, stop_id VARCHAR(255) NOT NULL COLLATE BINARY, trip_id VARCHAR(255) NOT NULL COLLATE BINARY, arrival DATETIME NOT NULL, departure DATETIME NOT NULL, PRIMARY KEY(stop_id, trip_id, sequence))');
$this->addSql('INSERT INTO trip_stop (sequence, arrival, departure, stop_id, trip_id) SELECT sequence, arrival, departure, stop_id, trip_id FROM __temp__trip_stop');
$this->addSql('DROP TABLE __temp__trip_stop');
}
}

View File

@ -1,27 +0,0 @@
<?php
use App\Kernel;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/config/bootstrap.php';
if ($_SERVER['APP_DEBUG']) {
umask(0000);
Debug::enable();
}
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) {
Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO);
}
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) {
Request::setTrustedHosts([$trustedHosts]);
}
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

View File

@ -1,29 +0,0 @@
FROM cojedzie/api:latest-rr
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN install-php-extensions xdebug-^3.0;
RUN apk add git;
# XDebug
RUN echo "xdebug.mode=debug" >> $PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini && \
echo "xdebug.client_host=172.17.0.1" >> $PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini && \
echo "xdebug.start_with_request=On" >> $PHP_INI_DIR/conf.d/docker-php-ext-xdebug.ini;
# Blackfire
RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \
&& curl -A "Docker" -o /tmp/blackfire-probe.tar.gz -D - -L -s https://blackfire.io/api/v1/releases/probe/php/linux/amd64/$version \
&& mkdir -p /tmp/blackfire \
&& tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \
&& mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \
&& printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8707\n" > $PHP_INI_DIR/conf.d/blackfire.ini \
&& rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz
# Timezone
RUN ln -snf /usr/share/zoneinfo/Europe/Warsaw /etc/localtime && \
echo "date.timezone = Europe/Warsaw" >> /usr/local/etc/php/conf.d/datetime.ini;
WORKDIR /var/www
EXPOSE 9001

View File

@ -1,48 +0,0 @@
<?php
namespace App\Command;
use App\Message\UpdateDataMessage;
use App\Service\DataUpdater;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\MessageBusInterface;
class UpdateCommand extends Command
{
/** @var DataUpdater */
private $updater;
/** @var MessageBusInterface */
private $bus;
public function __construct(DataUpdater $updater, MessageBusInterface $bus)
{
parent::__construct('app:update');
$this->updater = $updater;
$this->bus = $bus;
}
protected function configure()
{
$this->addOption(
'async', 'a',
InputOption::VALUE_NONE,
'Run in worker process via message queue.'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('async')) {
$this->bus->dispatch(new UpdateDataMessage());
$output->writeln("Update request sent to message queue.");
} else {
$this->updater->update($output);
}
return Command::SUCCESS;
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Exception\NonExistentServiceException;
use App\Service\Converter;
use App\Service\ProviderResolver;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use function Kadet\Functional\ref;
class ProviderController extends Controller
{
public function index(ProviderResolver $resolver, Converter $converter)
{
$providers = $resolver
->all()
->map(ref([$converter, 'convert']))
->values()
->toArray()
;
return $this->json($providers);
}
public function one(ProviderResolver $resolver, Converter $converter, $id)
{
try {
$provider = $resolver->resolve($id);
return $this->json($converter->convert($provider));
} catch (NonExistentServiceException $exception) {
throw new NotFoundHttpException($exception->getMessage());
}
}
}

View File

@ -1,98 +0,0 @@
<?php
namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Line;
use App\Model\Stop;
use App\Model\Track;
use App\Modifier\IdFilter;
use App\Modifier\RelatedFilter;
use App\Provider\TrackRepository;
use Swagger\Annotations as SWG;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use function App\Functions\encapsulate;
use function Kadet\Functional\ref;
/**
* @Route("/tracks")
* @SWG\Tag(name="Tracks")
*/
class TracksController extends Controller
{
/**
* @SWG\Response(
* response=200,
* description="Returns all tracks for specific provider, e.g. ZTM Gdańsk.",
* )
* @Route("/", methods={"GET"})
*/
public function index(Request $request, TrackRepository $repository)
{
$modifiers = $this->getModifiersFromRequest($request);
return $this->json($repository->all(...$modifiers));
}
/**
* @Route("/stops", methods={"GET"})
* @Route("/{track}/stops", methods={"GET"})
*
* @SWG\Tag(name="Tracks")
*
* @SWG\Response(response=200, description="Stops related to specified query.")
*/
public function stops(Request $request, TrackRepository $repository)
{
$modifiers = $this->getStopsModifiersFromRequest($request);
return $this->json($repository->stops(...$modifiers));
}
private function getModifiersFromRequest(Request $request)
{
if ($request->query->has('stop')) {
$stop = encapsulate($request->query->get('stop'));
$stop = collect($stop)->map([Stop::class, 'reference']);
yield new RelatedFilter($stop, Stop::class);
}
if ($request->query->has('line')) {
$line = encapsulate($request->query->get('line'));
$line = collect($line)->map([Line::class, 'reference']);
yield new RelatedFilter($line, Line::class);
}
if ($request->query->has('id')) {
$id = encapsulate($request->query->get('id'));
yield new IdFilter($id);
}
}
private function getStopsModifiersFromRequest(Request $request)
{
if ($request->query->has('stop')) {
$stop = encapsulate($request->query->get('stop'));
$stop = collect($stop)->map(ref([Stop::class, 'reference']));
yield new RelatedFilter($stop);
}
if ($request->query->has('track') || $request->attributes->has('track')) {
$track = $request->get('track');
$track = Track::reference($track);
yield new RelatedFilter($track);
}
if ($request->query->has('id')) {
$id = encapsulate($request->query->get('id'));
yield new IdFilter($id);
}
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Controller\Api\v1;
use App\Controller\Controller;
use App\Model\Trip;
use App\Modifier\IdFilter;
use App\Modifier\With;
use App\Provider\TripRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/trips")
*/
class TripController extends Controller
{
/**
* @Route("/{id}", methods={"GET"})
*/
public function one($id, TripRepository $repository)
{
$trip = $repository->first(new IdFilter($id), new With('schedule'));
return $this->json($trip, Response::HTTP_OK, [], $this->serializerContextFactory->create(Trip::class));
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Controller;
use App\Service\SerializerContextFactory;
use JMS\Serializer\SerializerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
abstract class Controller extends AbstractController
{
protected $serializer;
protected $serializerContextFactory;
public function __construct(SerializerInterface $serializer, SerializerContextFactory $serializerContextFactory)
{
$this->serializer = $serializer;
$this->serializerContextFactory = $serializerContextFactory;
}
protected function json($data, int $status = 200, array $headers = [], $context = null): JsonResponse
{
return new JsonResponse($this->serializer->serialize($data, "json", $context), $status, $headers, true);
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Event;
use App\Event\HandleModifierEvent;
use App\Modifier\Modifier;
use App\Provider\Repository;
use Doctrine\ORM\QueryBuilder;
class HandleDatabaseModifierEvent extends HandleModifierEvent
{
private $builder;
public function __construct(
Modifier $modifier,
Repository $repository,
QueryBuilder $builder,
array $meta = []
) {
parent::__construct($modifier, $repository, $meta);
$this->builder = $builder;
}
public function getBuilder(): QueryBuilder
{
return $this->builder;
}
public function replaceBuilder(QueryBuilder $builder): void
{
$this->builder = $builder;
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Event;
use App\Modifier\Modifier;
use App\Provider\Repository;
class HandleModifierEvent
{
private $repository;
private $modifier;
private $meta = [];
public function __construct(Modifier $modifier, Repository $repository, array $meta = [])
{
$this->repository = $repository;
$this->modifier = $modifier;
$this->meta = $meta;
}
public function getModifier(): Modifier
{
return $this->modifier;
}
public function getRepository()
{
return $this->repository;
}
public function getMeta(): array
{
return $this->meta;
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Event;
use App\Modifier\Modifier;
use App\Provider\Repository;
class PostProcessEvent extends HandleModifierEvent
{
private $data;
public function __construct($data, Modifier $modifier, Repository $repository, array $meta = [])
{
parent::__construct($modifier, $repository, $meta);
$this->data = $data;
}
public function getData()
{
return $this->data;
}
public function setData($data): void
{
$this->data = $data;
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Exception;
class InvalidArgumentException extends \InvalidArgumentException
{
public static function invalidType($parameter, $value, array $expected = [])
{
return new static(
sprintf('Expected %s to be of type: %s. %s given.', $parameter, implode(', ', $expected), gettype($value))
);
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Exception;
class NonExistentServiceException extends \LogicException
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Exception;
class NotSupportedException extends \LogicException
{
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Exception;
use App\Modifier\Modifier;
class UnsupportedModifierException extends \LogicException
{
public static function createFromModifier(Modifier $modifier)
{
return new static(sprintf("Modifier %s is not supported.", get_class($modifier)));
}
}

View File

@ -1,3 +0,0 @@
<?php
require_once __DIR__ . '/helpers.php';

View File

@ -1,63 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\ScheduledStop;
use App\Model\Stop;
use App\Modifier\FieldFilter;
use function App\Functions\encapsulate;
class FieldFilterDatabaseHandler implements ModifierHandler
{
protected $mapping = [
Stop::class => [
'name' => 'name',
],
ScheduledStop::class => [
'departure' => 'departure',
'arrival' => 'arrival',
]
];
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var FieldFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$field = $this->mapFieldName($event->getMeta()['type'], $modifier->getField());
$operator = $modifier->getOperator();
$value = $modifier->getValue();
$parameter = sprintf(":%s_%s", $alias, $field);
if ($operator === 'in' || $operator === 'not in') {
$parameter = "($parameter)";
$value = encapsulate($value);
}
$builder
->andWhere(sprintf("%s.%s %s %s", $alias, $field, $operator, $parameter))
->setParameter($parameter, $value)
;
}
protected function mapFieldName(string $class, string $field)
{
if (!isset($this->mapping[$class][$field])) {
throw new \InvalidArgumentException(
sprintf("Unable to map field %s of %s into entity field.", $field, $class)
);
}
return $this->mapping[$class][$field];
}
}

View File

@ -1,103 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\ScheduledStop;
use App\Model\Track;
use App\Model\TrackStop;
use App\Model\Trip;
use App\Modifier\RelatedFilter;
use App\Service\EntityReferenceFactory;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use function Kadet\Functional\Transforms\property;
class GenericWithDatabaseHandler implements ModifierHandler
{
protected $mapping = [
Track::class => [
'line' => 'line',
'stops' => 'stopsInTrack',
],
Trip::class => [
'schedule' => 'stops.stop',
],
TrackStop::class => [
'track' => 'track',
],
ScheduledStop::class => [
'trip' => 'trip',
'track' => 'trip.track',
'destination' => 'trip.track.final',
],
];
private $em;
private $id;
private $references;
public function __construct(
EntityManagerInterface $em,
IdUtils $idUtils,
EntityReferenceFactory $references
) {
$this->em = $em;
$this->id = $idUtils;
$this->references = $references;
}
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var RelatedFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$type = $event->getMeta()['type'];
if (!array_key_exists($modifier->getRelationship(), $this->mapping[$type])) {
throw new \InvalidArgumentException(
sprintf("Relationship %s is not supported for .", $type)
);
}
$relationship = $this->mapping[$type][$modifier->getRelationship()];
foreach ($this->getRelationships($relationship, $alias) as [$relationshipPath, $relationshipAlias]) {
$selected = collect($builder->getDQLPart('select'))->flatMap(property('parts'));
if ($selected->contains($relationshipAlias)) {
continue;
}
$builder
->join($relationshipPath, $relationshipAlias)
->addSelect($relationshipAlias);
}
}
/**
* @inheritDoc
*/
public static function getSubscribedServices()
{
return [
TrackByStopDatabaseHandler::class,
];
}
private function getRelationships($relationship, $alias)
{
$relationships = explode('.', $relationship);
foreach ($relationships as $current) {
yield [sprintf("%s.%s", $alias, $current), $alias = sprintf('%s_%s', $alias, $current)];
}
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Handler\ModifierHandler;
use App\Modifier\IdFilter;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Service\IdUtils;
use function Kadet\Functional\apply;
class IdFilterDatabaseHandler implements ModifierHandler
{
/**
* @var IdUtils
*/
private $id;
public function __construct(IdUtils $id)
{
$this->id = $id;
}
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var IdFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$provider = $event->getMeta()['provider'];
$id = $modifier->getId();
$mapper = apply([$this->id, 'generate'], $provider);
$builder
->andWhere($modifier->isMultiple() ? "{$alias} in (:id)" : "{$alias} = :id")
->setParameter(':id', $modifier->isMultiple() ? array_map($mapper, $id) : $mapper($id));
;
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Modifier\Limit;
class LimitDatabaseHandler implements ModifierHandler
{
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var Limit $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$builder
->setFirstResult($modifier->getOffset())
->setMaxResults($modifier->getCount())
;
}
}

View File

@ -1,107 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Model\Line;
use App\Model\ScheduledStop;
use App\Model\Stop;
use App\Model\Track;
use App\Model\TrackStop;
use App\Model\Trip;
use App\Modifier\RelatedFilter;
use App\Service\IdUtils;
use App\Service\EntityReferenceFactory;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class RelatedFilterDatabaseGenericHandler implements ModifierHandler, ServiceSubscriberInterface
{
protected $mapping = [
Track::class => [
Line::class => 'line',
Stop::class => TrackByStopDatabaseHandler::class,
],
TrackStop::class => [
Stop::class => 'stop',
Track::class => 'track',
],
ScheduledStop::class => [
Stop::class => 'stop',
Trip::class => 'trip',
],
];
private $em;
private $inner;
private $id;
private $references;
public function __construct(
ContainerInterface $inner,
EntityManagerInterface $em,
IdUtils $idUtils,
EntityReferenceFactory $references
) {
$this->inner = $inner;
$this->em = $em;
$this->id = $idUtils;
$this->references = $references;
}
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var RelatedFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$type = $event->getMeta()['type'];
if (!array_key_exists($type, $this->mapping)) {
throw new \InvalidArgumentException(
sprintf("Relationship filtering for %s is not supported.", $type)
);
}
if (!array_key_exists($modifier->getRelationship(), $this->mapping[$type])) {
throw new \InvalidArgumentException(
sprintf("Relationship %s is not supported for %s.", $modifier->getRelationship(), $type)
);
}
$relationship = $this->mapping[$type][$modifier->getRelationship()];
if ($this->inner->has($relationship)) {
/** @var ModifierHandler $inner */
$inner = $this->inner->get($relationship);
$inner->process($event);
return;
}
$parameter = sprintf(":%s_%s", $alias, $relationship);
$reference = $this->references->create($modifier->getRelated(), $event->getMeta()['provider']);
$builder
->join(sprintf('%s.%s', $alias, $relationship), $relationship)
->andWhere(sprintf($modifier->isMultiple() ? "%s in (%s)" : "%s = %s", $relationship, $parameter))
->setParameter($parameter, $reference);
}
/**
* @inheritDoc
*/
public static function getSubscribedServices()
{
return [
TrackByStopDatabaseHandler::class,
];
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Entity\TrackStopEntity;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\HandleModifierEvent;
use App\Handler\ModifierHandler;
use App\Modifier\RelatedFilter;
use App\Service\EntityReferenceFactory;
class TrackByStopDatabaseHandler implements ModifierHandler
{
private $references;
public function __construct(EntityReferenceFactory $references)
{
$this->references = $references;
}
public function process(HandleModifierEvent $event)
{
if (!$event instanceof HandleDatabaseModifierEvent) {
return;
}
/** @var RelatedFilter $modifier */
$modifier = $event->getModifier();
$builder = $event->getBuilder();
$alias = $event->getMeta()['alias'];
$relationship = 'stopsInTrack';
$parameter = sprintf(":%s_%s", $alias, $relationship);
$reference = $this->references->create($modifier->getRelated(), $event->getMeta()['provider']);
$condition = $modifier->isMultiple() ? 'stop_in_track.stop IN (%s)' : 'stop_in_track.stop = %s';
$builder
->join(sprintf("%s.%s", $alias, $relationship), 'stop_in_track')
->andWhere(sprintf($condition, $parameter))
->setParameter($parameter, $reference)
;
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace App\Handler\Database;
use App\Entity\TrackEntity;
use App\Event\PostProcessEvent;
use App\Handler\PostProcessingHandler;
use App\Model\Destination;
use App\Model\Stop;
use App\Service\CacheableConverter;
use App\Service\Converter;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use Illuminate\Support\Collection;
use Kadet\Functional as f;
use Kadet\Functional\Transforms as t;
class WithDestinationsDatabaseHandler implements PostProcessingHandler
{
private $em;
private $converter;
private $id;
public function __construct(EntityManagerInterface $entityManager, Converter $converter, IdUtils $id)
{
$this->em = $entityManager;
$this->converter = $converter;
$this->id = $id;
if ($this->converter instanceof CacheableConverter) {
$this->converter = clone $this->converter;
$this->converter->reset();
}
}
public function postProcess(PostProcessEvent $event)
{
$provider = $event->getMeta()['provider'];
$stops = $event
->getData()
->map(t\property('id'))
->map(f\apply([$this->id, 'generate'], $provider))
->all();
$destinations = collect($this->em->createQueryBuilder()
->select('t', 'tl', 'f', 'fs', 'ts')
->from(TrackEntity::class, 't')
->join('t.stopsInTrack', 'ts')
->join('t.line', 'tl')
->where('ts.stop IN (:stops)')
->join('t.final', 'f')
->join('f.stop', 'fs')
->getQuery()
->execute(['stops' => $stops]))
->reduce(function ($grouped, TrackEntity $track) {
foreach ($track->getStopsInTrack()->map(t\property('stop'))->map(t\property('id')) as $stop) {
$grouped[$stop] = ($grouped[$stop] ?? collect())->add($track);
}
return $grouped;
}, collect())
->map(function (Collection $tracks) {
return $tracks
->groupBy(function (TrackEntity $track) {
return $track->getFinal()->getStop()->getId();
})->map(function (Collection $tracks, $id) {
return Destination::createFromArray([
'stop' => $this->converter->convert($tracks->first()->getFinal()->getStop()),
'lines' => $tracks
->map(t\property('line'))
->unique(t\property('id'))
->map(f\ref([$this->converter, 'convert']))
->values(),
]);
})->values();
});
$event->getData()->each(function (Stop $stop) use ($provider, $destinations) {
$stop->setDestinations($destinations[$this->id->generate($provider, $stop->getId())]);
});
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Handler;
use App\Event\HandleModifierEvent;
interface ModifierHandler
{
public function process(HandleModifierEvent $event);
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Handler;
use App\Event\PostProcessEvent;
interface PostProcessingHandler
{
public function postProcess(PostProcessEvent $event);
}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Message;
final class UpdateDataMessage
{
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\MessageHandler;
use App\Message\UpdateDataMessage;
use App\Output\LoggerOutput;
use App\Service\DataUpdater;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final class UpdateDataMessageHandler implements MessageHandlerInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/** @var DataUpdater */
private $updater;
public function __construct(DataUpdater $updater)
{
$this->updater = $updater;
}
public function __invoke(UpdateDataMessage $message)
{
try {
$this->updater->update(new LoggerOutput($this->logger));
} catch (\Exception $exception) {
$this->logger->critical($exception->getMessage(), [
'backtrace' => $exception->getTraceAsString()
]);
}
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace App\Model;
use Illuminate\Support\Collection;
use JMS\Serializer\Annotation as Serializer;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
class Destination implements Fillable
{
use FillTrait;
/**
* Stop associated with destination.
* @Serializer\Type(Stop::class)
* @var Stop
*/
private $stop;
/**
* @Serializer\Type("Collection")
* @SWG\Property(type="array", @SWG\Items(ref=@Model(type=Line::class, groups={"Default"})))
* @var Line[]|Collection<Line>
*/
private $lines;
public function __construct()
{
$this->lines = collect();
}
public function getStop(): Stop
{
return $this->stop;
}
public function setStop(Stop $stop): void
{
$this->stop = $stop;
}
public function getLines(): Collection
{
return $this->lines;
}
public function setLines(iterable $lines): void
{
$this->lines = collect($lines);
}
}

View File

@ -1,117 +0,0 @@
<?php
namespace App\Model;
use Carbon\Carbon;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
class Provider implements Fillable, Referable
{
use FillTrait;
/**
* Short identifier of provider, ex. "trojmiasto"
* @SWG\Property(example="trojmiasto")
* @Serializer\Type("string")
* @var string
*/
private $id;
/**
* Full name of the provider, ex. "MZKZG Trójmiasto"
* @SWG\Property(example="MZKZG Trójmiasto")
* @Serializer\Type("string")
* @var string
*/
private $name;
/**
* Short name of the provider for easier identification, ex. "Trójmiasto" or "Warszawa"
* @SWG\Property(example="Trójmiasto")
* @Serializer\Type("string")
* @var string
*/
private $shortName;
/**
* Attribution to be presented for this provider, can contain HTML tags.
* @SWG\Property(example="Copyright by XYZ inc.")
* @Serializer\Type("string")
* @var string|null
*/
private $attribution;
/**
* Time when data was last synchronized with this provider.
* @Serializer\Type("Carbon")
* @var Carbon|null
*/
private $lastUpdate;
/**
* Location of provider centre of interest.
* @var Location
*/
private $location;
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getShortName(): string
{
return $this->shortName;
}
public function setShortName(string $shortName): void
{
$this->shortName = $shortName;
}
public function getAttribution(): ?string
{
return $this->attribution;
}
public function setAttribution(?string $attribution): void
{
$this->attribution = $attribution;
}
public function getLastUpdate(): ?Carbon
{
return $this->lastUpdate;
}
public function setLastUpdate(?Carbon $lastUpdate): void
{
$this->lastUpdate = $lastUpdate;
}
public function getLocation(): Location
{
return $this->location;
}
public function setLocation(Location $location): void
{
$this->location = $location;
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Model;
use Carbon\Carbon;
class ScheduledStop extends TrackStop
{
/**
* Arrival time.
* @var Carbon
*/
private $arrival;
/**
* Departure time.
* @var Carbon
*/
private $departure;
/**
* Exact trip that this scheduled stop is part of.
* @var Trip|null
*/
private $trip;
public function getArrival(): Carbon
{
return $this->arrival;
}
public function setArrival(Carbon $arrival): void
{
$this->arrival = $arrival;
}
public function getDeparture(): Carbon
{
return $this->departure;
}
public function setDeparture(Carbon $departure): void
{
$this->departure = $departure;
}
public function getTrip(): ?Trip
{
return $this->trip;
}
public function setTrip(?Trip $trip): void
{
$this->trip = $trip;
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Modifier;
class FieldFilter implements Modifier
{
private $field;
private $value;
private $operator;
public function __construct(string $field, $value, string $operator = '=')
{
$this->field = $field;
$this->value = $value;
$this->operator = $operator;
}
public static function contains(string $field, string $value)
{
return new static($field, "%$value%", 'LIKE');
}
public function getField(): string
{
return $this->field;
}
public function getValue()
{
return $this->value;
}
public function getOperator(): string
{
return $this->operator;
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Modifier;
use App\Exception\InvalidArgumentException;
use App\Modifier\Modifier;
use App\Service\IterableUtils;
class IdFilter implements Modifier
{
/** @var string|array */
private $id;
public function __construct($id)
{
if (!is_iterable($id) && !is_string($id)) {
throw InvalidArgumentException::invalidType('id', $id, ['string', 'array']);
}
$this->id = is_iterable($id) ? IterableUtils::toArray($id) : $id;
}
public function getId()
{
return $this->id;
}
public function isMultiple()
{
return is_array($this->id);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Modifier;
class Limit implements Modifier
{
private $offset;
private $count;
public function __construct(int $offset = 0, ?int $count = null)
{
$this->offset = $offset;
$this->count = $count;
}
public function getOffset()
{
return $this->offset;
}
public function getCount()
{
return $this->count;
}
public static function count(int $count)
{
return new static(0, $count);
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace App\Modifier;
interface Modifier
{
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Modifier;
use App\Exception\InvalidArgumentException;
use App\Model\Referable;
use App\Service\IterableUtils;
class RelatedFilter implements Modifier
{
private $relationship;
private $reference;
public function __construct($reference, ?string $relation = null)
{
if (!is_iterable($reference) && !$reference instanceof Referable) {
throw InvalidArgumentException::invalidType('object', $reference, [Referable::class, 'iterable']);
}
$this->reference = is_iterable($reference) ? IterableUtils::toArray($reference) : $reference;
$this->relationship = $relation ?: get_class($reference);
}
public function getRelationship(): string
{
return $this->relationship;
}
public function getRelated()
{
return $this->reference;
}
public function isMultiple()
{
return is_array($this->reference);
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Modifier;
class With implements Modifier
{
private $relationship;
public function __construct(string $relationship)
{
$this->relationship = $relationship;
}
public function getRelationship(): string
{
return $this->relationship;
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Output;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
use Symfony\Component\Console\Output\Output;
class LoggerOutput extends Output
{
/** @var LoggerInterface */
private $logger;
public function __construct(
LoggerInterface $logger,
?int $verbosity = self::VERBOSITY_NORMAL,
bool $decorated = false,
OutputFormatterInterface $formatter = null
) {
parent::__construct($verbosity, $decorated, $formatter);
$this->logger = $logger;
}
protected function doWrite(string $message, bool $newline)
{
$this->logger->info($message);
}
}

View File

@ -1,159 +0,0 @@
<?php
namespace App\Provider\Database;
use App\Entity\ProviderEntity;
use App\Event\HandleDatabaseModifierEvent;
use App\Event\PostProcessEvent;
use App\Handler\Database\FieldFilterDatabaseHandler;
use App\Handler\Database\IdFilterDatabaseHandler;
use App\Handler\Database\LimitDatabaseHandler;
use App\Handler\Database\RelatedFilterDatabaseGenericHandler;
use App\Handler\Database\GenericWithDatabaseHandler;
use App\Handler\ModifierHandler;
use App\Handler\PostProcessingHandler;
use App\Model\Referable;
use App\Modifier\FieldFilter;
use App\Modifier\IdFilter;
use App\Modifier\Limit;
use App\Modifier\Modifier;
use App\Modifier\RelatedFilter;
use App\Modifier\With;
use App\Provider\Repository;
use App\Service\Converter;
use App\Service\HandlerProvider;
use App\Service\IdUtils;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
abstract class DatabaseRepository implements Repository
{
const DEFAULT_LIMIT = 100;
/** @var EntityManagerInterface */
protected $em;
/** @var ProviderEntity */
protected $provider;
/** @var IdUtils */
protected $id;
/** @var Converter */
protected $converter;
/** @var HandlerProvider */
protected $handlers;
/**
* DatabaseRepository constructor.
*
* @param EntityManagerInterface $em
*/
public function __construct(
EntityManagerInterface $em,
IdUtils $id,
Converter $converter,
HandlerProvider $handlers
) {
$this->em = $em;
$this->id = $id;
$this->converter = $converter;
$this->handlers = $handlers;
$this->handlers->loadConfiguration(array_merge([
IdFilter::class => IdFilterDatabaseHandler::class,
Limit::class => LimitDatabaseHandler::class,
FieldFilter::class => FieldFilterDatabaseHandler::class,
RelatedFilter::class => RelatedFilterDatabaseGenericHandler::class,
With::class => GenericWithDatabaseHandler::class,
], static::getHandlers()));
}
/** @return static */
public function withProvider(ProviderEntity $provider)
{
$result = clone $this;
$result->provider = $provider;
return $result;
}
protected function convert($entity)
{
return $this->converter->convert($entity);
}
protected function reference($class, Referable $referable)
{
$id = $this->id->generate($this->provider, $referable->getId());
return $this->em->getReference($class, $id);
}
protected function processQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = [])
{
$reducers = [];
foreach ($modifiers as $modifier) {
$handler = $this->handlers->get($modifier);
if ($handler instanceof ModifierHandler) {
$event = new HandleDatabaseModifierEvent($modifier, $this, $builder, array_merge([
'provider' => $this->provider,
], $meta));
$handler->process($event);
}
if ($handler instanceof PostProcessingHandler) {
$reducers[] = function ($result) use ($meta, $modifier, $handler) {
$event = new PostProcessEvent($result, $modifier, $this, array_merge([
'provider' => $this->provider,
], $meta));
$handler->postProcess($event);
return $event->getData();
};
}
}
return collect($reducers);
}
protected function allFromQueryBuilder(QueryBuilder $builder, iterable $modifiers, array $meta = [])
{
$builder->setMaxResults(self::DEFAULT_LIMIT);
$reducers = $this->processQueryBuilder($builder, $modifiers, $meta);
$query = $builder->getQuery();
$paginator = new Paginator($query);
$result = collect($paginator)->map(\Closure::fromCallable([$this, 'convert']));
return $reducers->reduce(function ($result, $reducer) {
return $reducer($result);
}, $result);
}
public function first(Modifier ...$modifiers)
{
return $this->all(Limit::count(1), ...$modifiers)->first();
}
/**
* Returns array describing handlers for each modifier type. Syntax is as follows:
* [ IdFilter::class => IdFilterDatabaseHandler::class ]
*
* It is internally used as part of service subscriber.
*
* @return array
*/
protected static function getHandlers()
{
return [];
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Provider\Database;
use App\Entity\LineEntity;
use App\Model\Line;
use App\Modifier\Modifier;
use App\Provider\LineRepository;
use Illuminate\Support\Collection;
class GenericLineRepository extends DatabaseRepository implements LineRepository
{
public function all(Modifier ...$modifiers): Collection
{
$builder = $this->em
->createQueryBuilder()
->from(LineEntity::class, 'line')
->select('line')
;
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'line',
'entity' => LineEntity::class,
'type' => Line::class,
]);
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Provider\Database;
use App\Entity\OperatorEntity;
use App\Model\Operator;
use App\Modifier\Modifier;
use App\Provider\OperatorRepository;
use Illuminate\Support\Collection;
class GenericOperatorRepository extends DatabaseRepository implements OperatorRepository
{
public function all(Modifier ...$modifiers): Collection
{
$builder = $this->em
->createQueryBuilder()
->from(OperatorEntity::class, 'operator')
->select('operator')
;
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'operator',
'entity' => OperatorEntity::class,
'type' => Operator::class,
]);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Provider\Database;
use App\Entity\StopEntity;
use App\Handler\Database\GenericWithDatabaseHandler;
use App\Handler\Database\WithDestinationsDatabaseHandler;
use App\Model\Stop;
use App\Modifier\Modifier;
use App\Modifier\With;
use App\Provider\StopRepository;
use Illuminate\Support\Collection;
class GenericStopRepository extends DatabaseRepository implements StopRepository
{
public function all(Modifier ...$modifiers): Collection
{
$builder = $this->em
->createQueryBuilder()
->from(StopEntity::class, 'stop')
->select('stop')
;
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'stop',
'entity' => StopEntity::class,
'type' => Stop::class,
]);
}
protected static function getHandlers()
{
return array_merge(parent::getHandlers(), [
With::class => function (With $modifier) {
return $modifier->getRelationship() === 'destinations'
? WithDestinationsDatabaseHandler::class
: GenericWithDatabaseHandler::class;
},
]);
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Provider\Database;
use App\Entity\TrackEntity;
use App\Entity\TrackStopEntity;
use App\Model\Track;
use App\Model\TrackStop;
use App\Modifier\Modifier;
use App\Provider\TrackRepository;
use Illuminate\Support\Collection;
class GenericTrackRepository extends DatabaseRepository implements TrackRepository
{
public function stops(Modifier ...$modifiers): Collection
{
$builder = $this->em
->createQueryBuilder()
->from(TrackStopEntity::class, 'track_stop')
->select(['track_stop']);
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'track_stop',
'entity' => TrackStopEntity::class,
'type' => TrackStop::class,
]);
}
public function all(Modifier ...$modifiers): Collection
{
$builder = $this->em
->createQueryBuilder()
->from(TrackEntity::class, 'track')
->select('track');
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'track',
'entity' => TrackEntity::class,
'type' => Track::class,
]);
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Provider\Database;
use App\Entity\TripEntity;
use App\Model\Trip;
use App\Modifier\Modifier;
use App\Provider\TripRepository;
use Illuminate\Support\Collection;
class GenericTripRepository extends DatabaseRepository implements TripRepository
{
public function all(Modifier ...$modifiers): Collection
{
$builder = $this->em
->createQueryBuilder()
->from(TripEntity::class, 'trip')
->select('trip');
return $this->allFromQueryBuilder($builder, $modifiers, [
'alias' => 'trip',
'entity' => TripEntity::class,
'type' => Trip::class,
]);
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace App\Provider;
use App\Modifier\Modifier;
interface DepartureRepository extends Repository
{
public function current(iterable $stops, Modifier ...$modifiers);
}

View File

@ -1,12 +0,0 @@
<?php
namespace App\Provider;
use App\Modifier\Modifier;
use Illuminate\Support\Collection;
interface FluentRepository extends Repository
{
public function first(Modifier ...$modifiers);
public function all(Modifier ...$modifiers): Collection;
}

View File

@ -1,8 +0,0 @@
<?php
namespace App\Provider;
interface LineRepository extends FluentRepository
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Provider;
interface OperatorRepository extends FluentRepository
{
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Provider;
interface StopRepository extends FluentRepository
{
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Provider;
use App\Modifier\Modifier;
use Illuminate\Support\Collection;
interface TrackRepository extends FluentRepository
{
public function stops(Modifier ...$modifiers): Collection;
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Provider;
use App\Model\Trip;
interface TripRepository extends FluentRepository
{
}

View File

@ -1,224 +0,0 @@
<?php
namespace App\Provider\ZtmGdansk;
use App\Model\Departure;
use App\Model\Line;
use App\Model\ScheduledStop;
use App\Model\Stop;
use App\Model\Vehicle;
use App\Modifier\FieldFilter;
use App\Modifier\IdFilter;
use App\Modifier\Limit;
use App\Modifier\Modifier;
use App\Modifier\RelatedFilter;
use App\Modifier\With;
use App\Provider\DepartureRepository;
use App\Provider\LineRepository;
use App\Provider\ScheduleRepository;
use App\Service\IterableUtils;
use App\Service\ModifierUtils;
use App\Service\Proxy\ReferenceFactory;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Kadet\Functional\Transforms as t;
use function App\Functions\setup;
use function Kadet\Functional\ref;
class ZtmGdanskDepartureRepository implements DepartureRepository
{
const ESTIMATES_URL = 'http://ckan2.multimediagdansk.pl/delays';
/** @var LineRepository */
private $lines;
/** @var ReferenceFactory */
private $reference;
/** @var ScheduleRepository */
private $schedule;
/**
* @param LineRepository $lines
*/
public function __construct(LineRepository $lines, ScheduleRepository $schedule, ReferenceFactory $reference)
{
$this->lines = $lines;
$this->reference = $reference;
$this->schedule = $schedule;
}
public function current(iterable $stops, Modifier ...$modifiers)
{
$real = IterableUtils::toCollection($stops)
->flatMap(ref([$this, 'getRealDepartures']))
->sortBy(t\property('estimated'))
;
$now = Carbon::now()->second(0);
$first = $real->map(t\getter('scheduled'))->min() ?? $now;
$scheduled = $this->getScheduledDepartures($stops, $first, ...$this->extractModifiers($modifiers));
$result = $this->pair($scheduled, $real)->filter(function (Departure $departure) use ($now) {
return $departure->getDeparture() > $now;
});
return $this->processResultWithModifiers($result, $modifiers);
}
private function getRealDepartures(Stop $stop)
{
try {
$estimates = file_get_contents(static::ESTIMATES_URL . "?stopId=" . $stop->getId());
$estimates = json_decode($estimates, true)['delay'];
} catch (\Error $e) {
return collect();
}
$estimates = collect($estimates);
$lines = $estimates->map(function ($delay) {
return $delay['routeId'];
})->unique();
$lines = $this->lines->all(new IdFilter($lines))->keyBy(t\property('id'));
return collect($estimates)->map(function ($delay) use ($stop, $lines) {
$scheduled = (new Carbon($delay['theoreticalTime'], 'Europe/Warsaw'))->tz('UTC');
$estimated = (clone $scheduled)->addSeconds($delay['delayInSeconds']);
return Departure::createFromArray([
'key' => sprintf('%s::%s', $delay['routeId'], $scheduled->format('H:i')),
'scheduled' => $scheduled,
'estimated' => $estimated,
'stop' => $stop,
'display' => trim($delay['headsign']),
'vehicle' => $this->reference->get(Vehicle::class, $delay['vehicleCode']),
'line' => $lines->get($delay['routeId']) ?: Line::createFromArray([
'symbol' => $delay['routeId'],
'type' => Line::TYPE_UNKNOWN,
]),
]);
})->values();
}
private function getScheduledDepartures($stop, Carbon $time, Modifier ...$modifiers)
{
return $this->schedule->all(
new RelatedFilter($stop, Stop::class),
new FieldFilter('departure', $time, '>='),
new With('track'),
new With('destination'),
...$modifiers
);
}
private function pair(Collection $schedule, Collection $real)
{
$key = function ($departure) {
if ($departure instanceof Departure) {
return sprintf(
"%s::%s",
$departure->getLine()->getId(),
$departure->getScheduled()->format("H:i")
);
} elseif ($departure instanceof ScheduledStop) {
return sprintf(
"%s::%s",
$departure->getTrack()->getLine()->getId(),
$departure->getDeparture()->format("H:i")
);
} else {
throw new \Exception();
}
};
$schedule = $schedule->keyBy($key)->all();
$real = $real->keyBy($key);
return $real->map(function (Departure $real, $key) use (&$schedule) {
$scheduled = null;
if (array_key_exists($key, $schedule)) {
$scheduled = $schedule[$key];
unset($schedule[$key]);
}
return [
'estimated' => $real,
'scheduled' => $scheduled,
];
})->merge(collect($schedule)->map(function (ScheduledStop $scheduled) {
return [
'estimated' => null,
'scheduled' => $scheduled,
];
}))->map(function ($pair) {
return $this->merge($pair['estimated'], $pair['scheduled']);
})->sortBy(function (Departure $departure) {
$time = $departure->getEstimated() ?? $departure->getScheduled();
return $time->getTimestamp();
});
}
private function merge(?Departure $real, ?ScheduledStop $scheduled)
{
if (!$real) {
return $this->convertScheduledStopToDeparture($scheduled);
}
if (!$scheduled) {
return $real;
}
return setup(clone $real, function (Departure $departure) use ($scheduled, $real) {
$departure->setDisplay($this->extractDisplayFromScheduledStop($scheduled));
$departure->setTrack($scheduled->getTrack());
$departure->setTrip($scheduled->getTrip());
});
}
private function convertScheduledStopToDeparture(ScheduledStop $stop): Departure
{
return setup(new Departure(), function (Departure $converted) use ($stop) {
$converted->setDisplay($this->extractDisplayFromScheduledStop($stop));
$converted->setLine($stop->getTrack()->getLine());
$converted->setTrack($stop->getTrack());
$converted->setTrip($stop->getTrip());
$converted->setScheduled($stop->getDeparture());
$converted->setStop($stop->getStop());
});
}
private function extractDisplayFromScheduledStop(ScheduledStop $stop)
{
return $stop->getTrack()->getDestination()->getName();
}
private function extractModifiers(iterable $modifiers)
{
$result = [];
/** @var Limit $limit */
if ($limit = ModifierUtils::getOfType($modifiers, Limit::class)) {
$result[] = new Limit($limit->getOffset(), $limit->getCount() * 2);
} else {
$result[] = Limit::count(16);
}
return $result;
}
private function processResultWithModifiers(Collection $result, iterable $modifiers)
{
foreach ($modifiers as $modifier) {
switch (true) {
case $modifier instanceof Limit:
$result = $result->slice($modifier->getOffset(), $modifier->getCount());
break;
}
}
return $result;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Serialization;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"PROPERTY","METHOD","ANNOTATION"})
*/
class SerializeAs
{
/** @var array<string, string> @Required() */
public $map;
}

View File

@ -1,84 +0,0 @@
<?php
namespace App\Service;
use Illuminate\Support\Collection;
use function Kadet\Functional\Predicates\instance;
class AggregateConverter implements Converter, CacheableConverter
{
private $converters;
private $cachedConverters;
public function __construct(iterable $converters)
{
$this->converters = $converters;
}
public function convert($entity)
{
$this->ensureCachedConverters();
/** @var Converter $converter */
$converter = $this->cachedConverters->first(function (Converter $converter) use ($entity) {
return $converter->supports($entity);
});
if ($converter == null) {
throw new \InvalidArgumentException(sprintf('Cannot convert entity of type %s.', is_object($entity) ? get_class($entity) : gettype($entity)));
}
return $converter->convert($entity);
}
public function supports($entity)
{
return $this->converters->some(function (Converter $converter) use ($entity) {
return $converter->supports($entity);
});
}
public function getConverters(): Collection
{
$this->ensureCachedConverters();
return clone $this->cachedConverters;
}
public function reset()
{
$this->ensureCachedConverters();
$this
->cachedConverters
->filter(instance(CacheableConverter::class))
->each(function (CacheableConverter $converter) {
$converter->reset();
})
;
}
public function __clone()
{
$this->ensureCachedConverters();
$this->cachedConverters = $this->cachedConverters->map(function ($object) {
return clone $object;
});
}
private function ensureCachedConverters()
{
if (!$this->cachedConverters) {
$this->cachedConverters = collect($this->converters)
->filter(function (Converter $converter) {
return $converter !== $this && !$converter instanceof AggregateConverter;
})
->each(function (Converter $converter) {
if ($converter instanceof RecursiveConverter) {
$converter->setParent($this);
}
});
}
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Service;
use Symfony\Contracts\Service\ResetInterface;
interface CacheableConverter extends Converter, ResetInterface
{
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Service;
use App\Entity\Entity;
use App\Model\Line;
use App\Model\Operator;
use App\Model\ScheduledStop;
use App\Model\Stop;
use App\Model\Track;
use App\Model\Trip;
interface Converter
{
public function convert($entity);
public function supports($entity);
}

View File

@ -1,68 +0,0 @@
<?php
namespace App\Service;
use App\Entity\LineEntity;
use App\Entity\ProviderEntity;
use App\Entity\StopEntity;
use App\Entity\TrackEntity;
use App\Exception\InvalidArgumentException;
use App\Model\Line;
use App\Model\Referable;
use App\Model\Stop;
use App\Model\Track;
use Doctrine\ORM\EntityManagerInterface;
use Illuminate\Support\Collection;
use function Kadet\Functional\partial;
use function Kadet\Functional\ref;
use const Kadet\Functional\_;
final class EntityReferenceFactory
{
protected $mapping = [
Line::class => LineEntity::class,
Stop::class => StopEntity::class,
Track::class => TrackEntity::class,
];
private $em;
private $id;
public function __construct(EntityManagerInterface $em, IdUtils $id)
{
$this->em = $em;
$this->id = $id;
}
public function create($object, ProviderEntity $provider)
{
switch (true) {
case $object instanceof Referable:
return $this->createEntityReference($object, $provider);
case is_array($object):
return array_map(partial(ref([$this, 'createEntityReference']), _, $provider), $object);
case $object instanceof Collection:
return $object->map(partial(ref([$this, 'createEntityReference']), _, $provider));
default:
throw InvalidArgumentException::invalidType(
'object',
$object,
[Referable::class, Collection::class, 'array']
);
}
}
private function createEntityReference(Referable $object, ProviderEntity $provider)
{
$class = get_class($object);
if (!array_key_exists($class, $this->mapping)) {
throw new \InvalidArgumentException(sprintf("Cannot make entity reference of %s.", $class));
}
return $this->em->getReference(
$this->mapping[$class],
$this->id->generate($provider, $object->getId())
);
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Service;
use App\Exception\UnsupportedModifierException;
use App\Modifier\Modifier;
use Symfony\Component\DependencyInjection\ServiceLocator;
class HandlerProvider
{
private $configuration = [];
private $handlerLocator;
public function __construct(ServiceLocator $handlerLocator)
{
$this->handlerLocator = $handlerLocator;
}
public function loadConfiguration(array $providers)
{
$this->configuration = $providers;
}
public function get(Modifier $modifier)
{
$class = get_class($modifier);
if (!array_key_exists($class, $this->configuration)) {
throw UnsupportedModifierException::createFromModifier($modifier);
}
$handler = $this->configuration[$class];
if (is_callable($handler)) {
$handler = $handler($modifier);
}
if (is_string($handler)) {
return $this->handlerLocator->get($handler);
}
return $handler;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Service;
use Doctrine\Common\Collections\ArrayCollection;
use Illuminate\Support\Collection;
final class IterableUtils
{
public static function toArray(iterable $iterable): array
{
if (is_array($iterable)) {
return $iterable;
}
return iterator_to_array($iterable);
}
public static function toArrayCollection(iterable $iterable): ArrayCollection
{
return new ArrayCollection(static::toArray($iterable));
}
public static function toCollection(iterable $iterable): Collection
{
return collect($iterable);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Service;
use Kadet\Functional\Predicate;
use function Kadet\Functional\Predicates\instance;
final class ModifierUtils
{
public static function get(iterable $modifiers, Predicate $predicate)
{
return collect($modifiers)->first($predicate);
}
public static function getOfType(iterable $modifiers, $class)
{
return self::get($modifiers, instance($class));
}
public static function hasAny(iterable $modifiers, Predicate $predicate)
{
return collect($modifiers)->contains($predicate);
}
public static function hasAnyOfType(iterable $modifiers, $class)
{
return collect($modifiers)->contains(instance($class));
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Service;
use App\Model\Provider as ProviderDTO;
use App\Provider\Provider;
class ProviderConverter implements Converter
{
public function convert($entity)
{
/** @var Provider $entity */
return ProviderDTO::createFromArray([
'id' => $entity->getIdentifier(),
'shortName' => $entity->getShortName(),
'name' => $entity->getName(),
'attribution' => $entity->getAttribution(),
'lastUpdate' => $entity->getLastUpdate() ? clone $entity->getLastUpdate() : null,
'location' => $entity->getLocation(),
]);
}
public function supports($entity)
{
return $entity instanceof Provider;
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Service\Proxy;
final class FileLocator extends \ProxyManager\FileLocator\FileLocator
{
public function __construct(string $proxiesDirectory)
{
$absolutePath = realpath($proxiesDirectory);
if ($absolutePath === false) {
mkdir($proxiesDirectory, 0755, true);
}
parent::__construct($proxiesDirectory);
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Service;
interface RecursiveConverter extends Converter
{
public function setParent(?Converter $converter);
public function getParent(): ?Converter;
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Service;
trait RecursiveConverterTrait
{
/**
* @var Converter
*/
private $parent;
public function setParent(?Converter $converter)
{
$this->parent = $converter;
}
public function getParent(): ?Converter
{
return $this->parent;
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Service;
use App\Entity\TrackStopEntity;
use App\Entity\TripStopEntity;
use App\Model\ScheduledStop;
use App\Model\TrackStop;
class ScheduledStopConverter implements Converter, RecursiveConverter
{
use RecursiveConverterTrait;
public function convert($entity)
{
if ($entity instanceof TrackStopEntity) {
return TrackStop::createFromArray([
'stop' => $this->parent->convert($entity->getStop()),
'track' => $this->parent->convert($entity->getTrack()),
'order' => $entity->getOrder(),
]);
}
if ($entity instanceof TripStopEntity) {
return ScheduledStop::createFromArray([
'arrival' => $entity->getArrival(),
'departure' => $entity->getDeparture(),
'stop' => $this->parent->convert($entity->getStop()),
'order' => $entity->getOrder(),
'track' => $this->parent->convert($entity->getTrip()->getTrack()),
'trip' => $this->parent->convert($entity->getTrip()),
]);
}
}
public function supports($entity)
{
return $entity instanceof TripStopEntity
|| $entity instanceof TrackStopEntity;
}
}

View File

@ -1,81 +0,0 @@
<?php
namespace App\Service;
use App\Serialization\SerializeAs;
use Doctrine\Common\Annotations\Reader;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\SerializationContext;
use Metadata\AdvancedMetadataFactoryInterface;
use Metadata\ClassHierarchyMetadata;
use function Kadet\Functional\Transforms\property;
final class SerializerContextFactory
{
private $factory;
private $reader;
public function __construct(AdvancedMetadataFactoryInterface $factory, Reader $reader)
{
$this->factory = $factory;
$this->reader = $reader;
}
public function create($subject, array $groups = ['Default'])
{
return SerializationContext::create()->setSerializeNull(true)->setGroups($this->groups($subject, $groups));
}
private function groups($subject, array $groups)
{
$metadata = $this->factory->getMetadataForClass(is_object($subject) ? get_class($subject) : $subject);
$properties = $metadata instanceof ClassHierarchyMetadata
? collect($metadata->classMetadata)->flatMap(property('propertyMetadata'))
: $metadata->propertyMetadata;
$fields = [];
/** @var PropertyMetadata $property */
foreach ($properties as $property) {
try {
$annotation = $this->getAnnotationForProperty($property);
if ($annotation && !empty($fieldGroups = $this->map($annotation, $groups))) {
$type = $property->type;
$class = $type['name'] !== 'array' ? $type['name'] : $type['params'][0];
$fields[$property->name] = $this->groups($class, $fieldGroups);
}
} catch (\ReflectionException $e) { }
}
return array_merge($groups, $fields);
}
private function getAnnotationForProperty(PropertyMetadata $metadata)
{
$reflection = new \ReflectionClass($metadata->class);
try {
$property = $reflection->getProperty($metadata->name);
/** @var SerializeAs $annotation */
return $this->reader->getPropertyAnnotation($property, SerializeAs::class);
} catch (\ReflectionException $exception) {
$method = $reflection->getMethod($metadata->getter);
return $this->reader->getMethodAnnotation($method, SerializeAs::class);
}
}
private function map(SerializeAs $annotation, array $groups)
{
$result = [];
foreach ($groups as $group) {
if (array_key_exists($group, $annotation->map)) {
$result[] = $annotation->map[$group];
} else {
$result[] = $groups;
}
}
return $result;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Subscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class JSONFormatSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => "onRequest",
];
}
public function onRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->has('_format')) {
$request->attributes->set('_format', 'json');
}
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Subscriber;
use App\Service\Converter;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Contracts\Service\ResetInterface;
class RequestCleanupSubscriber implements EventSubscriberInterface
{
/** @var ContainerInterface */
private ContainerInterface $container;
private Converter $converter;
public function __construct(ContainerInterface $container, Converter $converter)
{
$this->container = $container;
$this->converter = $converter;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::TERMINATE => ['onTerminate']
];
}
public function onTerminate(TerminateEvent $event)
{
$this->container->get('doctrine')->reset();
if ($this->converter instanceof ResetInterface) {
$this->converter->reset();
}
}
}

Some files were not shown because too many files have changed in this diff Show More