Problem z aktualizacją widżetu Jakdojade

Jakdojade to jedna z najczęściej używanych przeze mnie aplikacji na telefonie. Na jednym ekranie mam dwa widżety z przystankami, z których najczęściej korzystam. Niestety po wymianie telefonu (Xiaomi Redmi 3S) godziny odjazdów w widżetach przestały być aktualizowane.

Czasami chwilowo pomagało otworzenie obserwowanych przystanków w aplikacji, ale po kilk lub kilkunastu godzinach dane znowu były nieaktualne. Myślałem, że to może być problem z uprawnieniami aplikacji do korzystania z transferu danych, jednak to był błędny trop.

Skuteczne rozwiązanie także było związane z uprawnieniami, ale do innej czynności, a mianowicie autostartu. Po dodaniu aplikacji Jakdojade do autostartu godziny odjazdów w widżetach są regularnie aktualizowane.

Konfigurowanie identyfikatora sesji w PHP

Rok temu pisałem o łączeniu staromodnej aplikacji z Laravelem. Po zmianach w konfiguracji środowiska PHP wypłynął nowy problem, a mianowicie Laravelowi nie podobały się identyfikatory sesji generowane przez starą aplikację, więc w ich miejsce generował nowe tym samym skutecznie niwecząc dzielenie sesji.

Identyfikatory generowane przez starą aplikację wyglądały np. tak: mh3e2fj757ed8nnlukqf5ag0f3. Tymczasem Laravel (konkretnie wersja 5.1) za jedynie słuszne uznaje identyfikatory składające się z 40 znaków (cyfry i litery od a do f). Takie zachowanie zdefiniowane jest w metodzie \Illuminate\Session\Store::isValid():

public function isValidId($id)
{
   return is_string($id) && preg_match('/^[a-f0-9]{40}$/', $id);
}

Rozwiązaniem tego problemu jest takie skonfigurowanie sesji PHP-owych, by identyfikatory spełniały wymagania Laravela 5.1:

ini_set('session.hash_function', 'sha1');
ini_set('session.hash_bits_per_character', '4');

Problem z repozytorium pakietów Opery

Przy próbie wykonania aktualizacji listy pakietów aptitude zakomunikował mi rzecz następującą:

E: The repository 'https://deb.opera.com/opera-stable stable Release' does no longer have a Release file.

Najwyraźniej w repozytorium Opery zaszły jakieś zmiany. Zajrzałem na adres repozytorium i faktycznie we wskazanym katalogu pliku Release nie było, ale za to w podkatalogu opera znajdowały się wszystkie niezbędne pliki. Wobec tego otworzyłem plik /etc/apt/sources.list.d/opera-stable.list i poniższą linijkę:

deb https://deb.opera.com/opera-stable/ stable non-free #Opera Browser (final releases)

zamieniłem na taką:

deb https://deb.opera.com/opera-stable/opera testing non-free

Po tym zabiegu aktualizacja przebiegła bez problemów.

Dodam, że korzystam z Debiana w wersji testing (obecnie stretch) i dlatego podałem taką dystrybucję.

PrestaShop 1.6 na PHP 7.0

Zgodnie z planem przenoszę kolejne strony na PHP 7.0. W ten weekend przyszła kolej na eduNagrody.pl, który to sklepik stoi na PrestaShop 1.6. Wedle autorów wersja 1.6.1.4 przynosi kompatybilność z PHP 7. Moja instalacja ma raptem kilka dodatkowych modułów, więc założyłem, że z przesiadką nie będzie problemów.

Podobnie jak w przypadku WordPressa, przyspieszenie generowania stron przez PrestaShop jest odczuwalne. Testy przez ab pokazują mniej więcej dwukrotnie lepsze wyniki.

PHP 5.6

Server Software:        nginx/1.8.1
Server Hostname:        edunagrody.pl
Server Port:            443
SSL/TLS Protocol:       TLSv1/SSLv3,ECDHE-RSA-AES256-GCM-SHA384,2048,256

Document Path:          /
Document Length:        76033 bytes

Concurrency Level:      4
Time taken for tests:   9.500 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      7664764 bytes
HTML transferred:       7603300 bytes
Requests per second:    10.53 [#/sec] (mean)
Time per request:       379.995 [ms] (mean)
Time per request:       94.999 [ms] (mean, across all concurrent requests)
Transfer rate:          787.92 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       38   63  15.1     64      93
Processing:   194  312  60.4    309     456
Waiting:      157  276  61.5    281     420
Total:        237  375  62.4    375     521

Percentage of the requests served within a certain time (ms)
  50%    375
  66%    402
  75%    415
  80%    428
  90%    457
  95%    500
  98%    520
  99%    521
 100%    521 (longest request)

PHP 7.0

Server Software:        nginx/1.8.1
Server Hostname:        edunagrody.pl
Server Port:            443
SSL/TLS Protocol:       TLSv1/SSLv3,ECDHE-RSA-AES256-GCM-SHA384,2048,256

Document Path:          /
Document Length:        76033 bytes

Concurrency Level:      4
Time taken for tests:   4.415 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      7664708 bytes
HTML transferred:       7603300 bytes
Requests per second:    22.65 [#/sec] (mean)
Time per request:       176.616 [ms] (mean)
Time per request:       44.154 [ms] (mean, across all concurrent requests)
Transfer rate:          1695.22 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       38   44   7.1     42      78
Processing:    79  130  36.0    129     278
Waiting:       54  100  37.2     98     253
Total:        119  175  36.1    171     328

Percentage of the requests served within a certain time (ms)
  50%    171
  66%    182
  75%    188
  80%    194
  90%    212
  95%    227
  98%    295
  99%    328
 100%    328 (longest request)

PHP 7.0 na blogasku

Postanowiłem dać szansę PHP 7.0 na serwerze produkcyjnym, a konkretnie na WordPressie napędzającym tegoż blogaska. W końcu od premiery minęło jakieś dwa i pół miesiąca, w tym czasie bolączki wieku dziecięcego chyba powinny zostać wyeliminowane. Co więcej, próby na moich testowych maszynach wirtualnych nie wykazały żadnych problemów, za to pokazały wyraźne przyspieszenie.

Przesiadka jest łatwa i w pełni odwracalna, PHP 7.0 z dotdeb może działać równolegle do PHP 5.6 od Debiana, zatem wystarczy w konfiguracji vhosta zmienić ścieżkę do interpretera. Różnica w prędkości generowania stron jest wyraźnie odczuwalna organoleptycznie, ale jeśli do kogoś bardziej przemawiają liczby to poniżej wrzucam szybki test wykonany ab.

PHP 5.6

Server Hostname:        michal.durys.pl
Server Port:            80

Document Path:          /
Document Length:        65737 bytes

Concurrency Level:      4
Time taken for tests:   16.142 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      6594000 bytes
HTML transferred:       6573700 bytes
Requests per second:    6.20 [#/sec] (mean)
Time per request:       645.680 [ms] (mean)
Time per request:       161.420 [ms] (mean, across all concurrent requests)
Transfer rate:          398.93 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       11   11   0.6     11      17
Processing:   281  630 143.4    627    1083
Waiting:      111  287  77.4    288     674
Total:        292  641 143.5    638    1094

Percentage of the requests served within a certain time (ms)
  50%    638
  66%    706
  75%    767
  80%    780
  90%    804
  95%    824
  98%    879
  99%   1094
 100%   1094 (longest request)

PHP 7.0

Server Software:        nginx/1.8.1
Server Hostname:        michal.durys.pl
Server Port:            80

Document Path:          /
Document Length:        65737 bytes

Concurrency Level:      4
Time taken for tests:   6.343 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      6594000 bytes
HTML transferred:       6573700 bytes
Requests per second:    15.77 [#/sec] (mean)
Time per request:       253.721 [ms] (mean)
Time per request:       63.430 [ms] (mean, across all concurrent requests)
Transfer rate:          1015.20 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       11   11   0.2     11      12
Processing:    63  241  92.4    219     557
Waiting:       34  123  45.1    109     206
Total:         74  252  92.3    230     568

Percentage of the requests served within a certain time (ms)
  50%    230
  66%    288
  75%    307
  80%    320
  90%    365
  95%    411
  98%    501
  99%    568
 100%    568 (longest request)

Tak na oko, przyspieszenie jest blisko dwu i półkrotne, wszystko działa, w logach czysto, więc wygląda to obiecująca. Jeśli w najbliższych dniach nie wystąpią problemy dam szansę siódemce z Piwikiem, PrestaShop i Roundcube’em.

Wyłączenie funkcji buforowania PrestaShop

PrestaShop posiada przydatny moduł pod tytułem „Aktualizacja 1 kliknięciem”. Aby takową aktualizację uruchomić należy spełnić kilka warunków.

Aktualizacja 1 kliknięciem

O ile przełączyć sklep w tryb konserwacji można z poziomu rzeczonej listy, o tyle sprawnie, że „Funkcje buforowania PrestaShop są wyłączone” nie jest takie oczywiste. PrestaShop ma kilka ustawień, które związane są z buforowaniem czy też pamięcią podręczną, a w polskim tłumaczeniu żadna z nich nie nazywa się „funkcje buforowania”. Żeby spełnić ten warunek należy udać się do menu Zaawansowane / Wydajność, a następnie zjechać na dół strony do panelu zatytułowanego Cache. Tam należy przestawić suwaczek „Użyj pamięci podręcznej” w pozycję „NIE”.

Użyj pamięci podręcznej

Android zacina się? Reset do ustawień fabrycznych może pomóc

Swego czasu jojczyłem na blogasku na ślamazarnie działającego Androida 5.0 na mojej Motorolce Moto G pierwszej generacji. Opisywane czyszczenie partycji cache działało doraźnie, Android 5.1 nic w tej kwestii nie zmienił, a moja irytacja rosła, aż w końcu urosła do tego stopnia, że pokonała moje lenistwo. Kilka dni temu zgrałem z telefonu wszystko, co uznałem za istotne i zaordynowałem reset do stanu fabrycznego. Efekt? Telefon działa tak jak po wyjęciu z pudełka, po kilku dniach działania nie wykazuje żadnych oznak zadyszki, a nawet nieco oszczędniej obchodzi się z baterią. Wygląda na to, że jednak nie będę miał sensownego powodu by wymieniać telefon w najbliższym czasie.

Operacja resetowania telefonu trwa krótko, ale ponowne konfigurowanie telefonu jest już dość czasochłonne. Na szczęście duża część może odbyć się automatycznie, o ile zezwolimy Googlowi na przechowywanie kopii zapasowych. Po zalogowaniu na konto Google’a telefon sam zainstalował wcześniej używane aplikacje, przywrócił tapetę i część ustawień.

Dla porządku zamieszczam skróconą instrukcję:

  1. Wyłączyć telefon.
  2. Przytrzymać przez kilka sekund jednocześnie przyciski zwiększania głośności, zmniejszania głośności i włącznik.
  3. Używając przycisku zmniejszania głośności w menu należy podświetlić opcję Recovery, a następnie zatwierdzić wybór przyciskiem zwiększania głośności.
  4. Gdy na ekranie pojawi się leżący na plecach android oraz napis „no command” należy przytrzymać włącznik i nacisnąć przycisk zwiększania głośności.
  5. Na ekranie objawi się menu trybu Recovery, z którego trzeba wybrać „Factory Reset”. Wybór zatwierdza się przyciskiem włączania telefonu.
  6. W kolejnym menu należy zatwierdzić operację w ten sam sposób.

Łączenie starej aplikacji z Laravelem

Najciewkawszą rzeczą, którą programowałem ostatnimi czasy, było połączenie kodu dużej i nieco już wiekowej aplikacji z Laravelem, w oparciu o którego planowany jest jej dalszy rozwój.

Do tego problemu można podejść na wiele sposobów, nie ma uniwersalnego rozwiązania, wiele zależy od tego jak wyglądają aplikacje, które chcemy pożenić. W moim wypadku stara aplikacja jest bardzo różnorodna. Jej najstarsza część nawet nie jest obiektowa, nie ma żadnych warstw, to staroszkolne PHP przeplatane HTML-em, z całym gąszczem zmiennych globalnych i konfiguracją opartą na stałych. Nowsza część została zamknięta w miarę sesnownie zaprojektowanych obiektach na modłę Domain Driven Design. Na szczęście już wcześniej stara aplikacja została zrefaktoryzowana o tyle, że punktem wejścia jest jeden kontroler frontowy.

W moich rozważaniach dość szybko odrzuciłem postawienie aplikacji obok siebie (serwery na osobnych IP albo portach) i wykonywanie zapytań z nowej aplikacji do starej. W tym wariancie kod obu aplikacji jest rozdzielony, a ja chciałem skorzystać w nowej aplikacji z sensownej części starej aplikacji (DDD).

Korzystając z faktu, że stara aplikacja ma coś na kształt kontrolera frontowego postanowiłem wrzucić tam kod kontrolera frontowego z Laravela. Ponieważ za 95% akcji odpowiada stara aplikacja, logicznym byłoby umieszczenie laravelowego kodu na końcu, tak by nowa aplikacja była uruchamiana tylko kiedy stara aplikacja nie potrafi obsłużyć zapytania. Niestety, stary kod ustawia nagłówki HTTP, ustawia buforowanie wyjścia danych, robi obsługę błędów przez die() i robi kilka innych rzeczy, które brużdżą nowoczesnym aplikacjom, więc musiałem odwrócić schemat: najpierw uruchamiana jest nowa aplikacja, a jeśli ona nie potrafi obsłużyć zapytania to przekazuje kontrolę do starej aplikacji.

Najważniejszą zmianę wykonałem w app/Http/Kernel.php. Zmieniłem sposób obsługi wyjątku NotFoundHttpException. Normalnie ten wyjątek, jak każdy inny jest obsługiwany wewnątrz aplikacji przez wyświetlenie komunikatu błędu. Po zmianie wyjątek wyrzucany jest poza metodę handle().

public function handle($request)
{
	try {
		$request->enableHttpMethodParameterOverride();
		$response = $this->sendRequestThroughRouter($request);
	} catch (NotFoundHttpException $e) {
		throw $e;
	} catch (\Exception $e) {
		$this->reportException($e);
		$response = $this->renderException($request, $e);
	} catch (\Throwable $e) {
		$e = new \Symfony\Component\Debug\Exception\FatalThrowableError($e);
		$this->reportException($e);
		$response = $this->renderException($request, $e);
	}
	$this->app['events']->fire('kernel.handled', [$request, $response]);
	return $response;
}

Ze wspomnianego wyjątku możemy zrobić użytek w kontrolerze frontowym czyli w pliku public/index.php.

$errorReporting = ini_get('error_reporting');

require __DIR__.'/../../bootstrap/autoload.php';
$app = require_once __DIR__.'/../../bootstrap/app.php';
try {
	/** @var $kernel App\Http\Kernel */
	$kernel   = $app->make(Illuminate\Contracts\Http\Kernel::class);
	$response = $kernel->handle(
		$request = Illuminate\Http\Request::capture()
	);
	$response->send();
	$kernel->terminate($request, $response);
	exit;
} catch (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e) {
	unset($e);
}

ini_set('error_reporting', $errorReporting);
unset($errorReporting, $request, $response, $kernel, $app);

// poniżej znajduje się kod starej aplikacji

Najważniejszą zmianą jest opakowanie standardowego kontrolera frontowego w blok try catch. Jeśli laravelowy kernel wyrzuci NotFoundHttpException to znaczy, że nowa aplikacja nie obsługuje danej akcji i kontrolę należy przekazać do starej aplikacji. Dlatego w catchu nie ma żadnej obsługi błędów, a jedynie usunięcie wyjątku.

W standardowym kontrolerze Laravela ostatnią linią kodu jest wywołanie metody terminate(). Ponieważ w mojej wersji kontrolera dalej jest kod starej aplikacji dodałem funkcję exit(), dzięki czemu wszystko działa jak trzeba.

Kolejną zmianą jest zapamiętanie ustawienia error_reporting w zmiennej przed odpaleniem Laravela. Jest to niezbędne ponieważ w starym kodzie jest sporo kwiatków, które przy najbardziej restrykcyjnych ustawieniach raportowania błędów co i rusz wywalają aplikację. Laravel sam z siebie ustawia właśnie najbardziej restrykcyjne raportowanie błędów. Dlatego jeśli nowa aplikacja nie obsługuje danej akcji przed przejściem do starej aplikacji przywracam bezpieczną wartość error_reporting.

Ostatnią zmianą jest usunięcie wszystkich zmiennych zadeklarowanych przez kod Laravela. To raczej dmuchanie na zimne, bo i bez tego wszystko działało, ale wolę by dla starej aplikacji obecność Laravela była niezauważalna.

Po tych zmianach podstawowe zadanie jest już zrealizowane: akcje obsługiwane przez nową aplikację są obsługiwane przez Laravela, a wszystko co nie jest przez nią obsługiwane trafia do starej aplikacji. Jednak do pełni szczęścia jeszcze trochę brakuje.

Jednym z problemów jest dzielenie sesji pomiędzy starą i nową aplikację. W starej aplikacji sesję przechowuje memcached, co jest konfigurowane przez w php.ini. Laravel ma własne ustawienia sesji konfigurowane w pliku .env oraz config/session.php. W tym pierwszym zmieniłem sterownik sesji na memcached:

SESSION_DRIVER=memcached

W drugim pliku zmiany były następujące:

'cookie' => 'PHPSESSID',
'domain' => '.' . env(APP_DOMAIN),

Celem tych zmian jest dostosowanie parametrów sesji w Laravelu do ustawień starej aplikacji. Ku memu zaskoczeniu trzeba też było wykonać zmianę w config/cache.php.

'prefix' => '',

Okazuje się, że w wypadku trzymania sesji w memcached do klucza dodawany jest właśnie ten prefiks. Bez tej zmiany dla tego samego ID sesji (z ciasteczka) nowa i stara aplikacja budowały inne klucze dla memcached i przez to nie widziały tych samych danych.

Kolejny problem dotyczył wbudowanego w Laravela mechanizmu przeciwdziałania atakom typu Cross Site Request Forgery. Wymaga on tokena przechowywanego w sesji, którego stara aplikacja nie generuje. W rezultacie po przejściu ze strony w starej aplikacji na stronę z nowej aplikacji rzucany był wyjątek VerifyCsrfToken. Nie jestem dumny z tego rozwiązania, ale zdecydowałem się odłożyć rozwiązanie tego problemu na później przez wyłączenie tego mechanizmu po stronie Laravela. W app/Http/Kernel.php wykomentowałem stosowny middleware.

protected $middleware = [
	// ...
	// \Miinto\Http\Middleware\VerifyCsrfToken::class
];

Oczywiście to nie koniec wyzwań związanych z łączeniem obu aplikacji. Skoro już mamy Laravela to można korzystać z jego zalet, m.in. service containera, żeby nie musieć budować wszystkich zależności ręcznie. To z kolei rodzi konieczność refaktoryzowania starego kodu i rugowania zmiennych globalnych, stałych i innych naleciałości, ale to już temat na osobną notkę.

Walutowa karta prepaid Cinkciarz.pl

Dolarową kartę przedpłaconą kupiłem do cinkciarza, żeby płacić w amerykańskich sklepach (głównie Amazon.com) unikając podwójnego przewalutowania i bankowych kursów walut. Chociaż cel ten osiągnąłem to jednak karta nie do końca spełniła moje oczekiwania.

Największym rozczarowaniem okazało się zasilanie rachunku karty. Założyłem, że skoro karta jest sygnowana logiem Cinkciarz.pl to zasilenie rachunku karty z tegoż serwisu będzie się odbywać natychmiastowo. Nic bardziej mylnego. Od złożenia zlecenia do zaksięgowania wpłaty w mBanku, który obsługuje moją kartę, może minąć do 24 godzin. W praktyce oznacza to, że zakupy trzeba planować z wyprzedzeniem albo stale trzymać na rachunku karty odpowiednia kwotę. To trochę kłóci się z moim wyobrażeniem o kartach prepaid, których największą zaletą jest to, że nie da się z nich nic ukraść, bo doładowuje się je na konkretny zakup.

Konsekwencją czasu księgowania wpłat jest to, że z karty nie można skorzystać od razu po jej otrzymaniu. Karta jest bowiem aktywowana dopiero po zaksięgowaniu pierwszego zasilenia. Ja wybrałem się na zakupy od razu po rozpakowaniu karty. Wprawdzie wysłałem środki na rachunek karty, ale nie zostały one zaksięgowane na czas i Amazon poinformował mnie, że są problemy z płatnością wybrana kartą. Jak sądzę, wielu właścicieli tych kart mogło popełnić podobny błąd, bo przy wykonywaniu zasilenia nie ma żadnego komunikatu jasno określającego kiedy operacja zostanie wykonana.

Ostatnią wadą związaną z obsługą rachunku karty jest to, że nie ma prostego sposobu na przelanie dolarów z karty na moje konto. Można skorzystać z polskiego bankomatu żeby wypłacić złotówki, co jest oczywiście bez sensu; można skorzystać z zagranicznego bankomatu, który wypłaca dolary, co ma więcej sensu, ale i tak obciążone jest prowizją; w końcu można poczekać na wygaśniecie karty – wtedy pozostałe środki zostaną przelane na wskazane przy rejestracji karty konto. Żaden z tych sposobów nie nadaje się do częstego manewrowania saldem karty.

Drobiazgiem, który również może być zaklasyfikowany jako mankament jest fakt, że ważność karty to niecałe dwa lata. W moim wypadku różnica wynosi trzy miesiące, ale jeśli mBank lub Cinkciarz.pl zamawiają karty na zapas to komuś może trafić się karta z jeszcze krótszym terminem przydatności.

Oczywiście karta walutowa Cinkciarz.pl ma również zalety. Przede wszystkim transakcje walutowe wykonywane są bez przewalutowań. Walutę na rachunek karty można kupować po korzystniejszym niż bankowy kursie. Karta przedpłacona od cinkciarza może być także tańsza w eksploatacji niż bankowa karta do rachunku walutowego ponieważ płaci się za nią jednorazowo tylko 15 zł i przez kolejne (niecałe) dwa lata nie ma innych opłat (rocznych, miesięcznych, za brak wymaganego obrotu albo za brak wymaganej liczby transakcji).

W moim wypadku zalety cinkciarskiej karty przeważają nad jej wadami, jednak dla innych klientów z innymi priorytetami bilans może wyglądać inaczej. Dlatego warto znać zarówno wady jak i zalety, żeby podjąć odpowiednią decyzję. Mam nadzieję, że ten wpis to ułatwi.

Routing Symfony w skryptach JavaScript

FOSJsRoutingBundle to paczka, która rozwiązuje problem odwoływania się do aplikacji Symfony z poziomu skryptów JavaScript, np. przy definiowaniu akcji AJAX-owych. Problem może nie jest wielki, bo oczywiście można URL-e zapisać na sztywno, ale w takim wypadku nie można korzystać z dobrodziejstw środowiska dev.

Dzięki wspomnianej paczce URL do akcji w Symfony można wygenerować podobnie jak w PHP:

var url = Routing.generate('my_route_to_expose', { id: 10, foo: "bar" });

FOSJsRoutingBundle umożliwia zarówno dynamiczne jak i statyczne generowanie tras. W tym pierwszym wypadku aplikacja Symfony jest dynamicznie odpytywana o trasy, dzięki czemu zmiany w aplikacji od razu są widoczne w JavaScriptcie. Ten tryb warto wybrać w czasie developmentu albo w zastosowaniach, gdzie nieco mniejsza wydajność nie będzie problemem. W trybie statycznym należy poleceniem z konsoli wygenerować plik zawierający definicję routingu dla JS.

Poniżej opiszę instalację paczki w Symfony i konfigurację w trybie statycznym.

Tradycyjnie zacząłem od instalacji paczki composerem:

composer require friendsofsymfony/jsrouting-bundle

Po zainstalowaniu paczki dodałem ją do app/AppKernel.php:

public function registerBundles()
{
    $bundles = array(
        // ...
        new FOS\JsRoutingBundle\FOSJsRoutingBundle()
    );
}

Kolejnym krokiem jest skopiowanie statycznych plików do katalogu web:

app/console assets:install

Połączenie pomiędzy Symfony i JavaScriptem stanowią dwa skrypty, które dodałem do podstawowego szablonu strony:

<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="{{ asset('js/fos_js_routes.js') }}"></script>

Następnie należy w definicji routingu wskazać, które trasy mają być dostępne w JavaScriptcie. Dokumentacja pokazuje konfigurację YAML-ową, ja zaś używam annotacji:

/**
 * @Route("/sugeruj-produkt", name="suggest_product", options={"expose": true})
 */

Ostatnim krokiem jest wygenerowanie pliku z routingiem:

app/console fos:js-routing:dump

Teraz wszystko będzie grać i buczeć.