feature/micro-kernel-trait #2

Merged
krzysiej merged 2 commits from feature/micro-kernel-trait into master 2024-10-19 16:38:31 +02:00
16 changed files with 1594 additions and 74 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/vendor/ /vendor/
.idea .idea
database.sqlite database.sqlite
var/cache/

View File

@@ -4,10 +4,12 @@
"symfony/var-dumper": "^7.0", "symfony/var-dumper": "^7.0",
"illuminate/database": "11.26.0.0", "illuminate/database": "11.26.0.0",
"ext-json": "*", "ext-json": "*",
"twig/twig": "3.14.0.0",
"symfony/console": "^7.0", "symfony/console": "^7.0",
"symfony/routing": "^7.1", "symfony/routing": "^7.1",
"laravel/serializable-closure": "^1.3" "laravel/serializable-closure": "^1.3",
"symfony/http-kernel": "^7.1",
"symfony/framework-bundle": "^7.1",
"symfony/twig-bundle": "^7.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

1449
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,45 @@
<?php <?php
include_once 'vendor/autoload.php'; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Krzysiej\RyobiCrawler\Controller\CategoryController; require 'vendor/autoload.php';
use Krzysiej\RyobiCrawler\Controller\IndexController;
use Krzysiej\RyobiCrawler\Controller\ProductController;
use Krzysiej\RyobiCrawler\Controller\PromosController;
use Krzysiej\RyobiCrawler\Controller\SearchController;
use Krzysiej\RyobiCrawler\Controller\StarController;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
if (!file_exists('database.sqlite')) { class Kernel extends BaseKernel
exit('Database file <code>database.sqlite</code> missing. Run docker compose <blockquote>docker compose exec php-app php index.php app:migrate</blockquote> to create it.'); {
} use MicroKernelTrait;
$productRoute = new Route('/product/{product_id<\d+>}', ['_controller' => ProductController::class]);
$searchRoute = new Route('/?search={search_term}', ['_controller' => SearchController::class]);
$categoryRoute = new Route('/category/{category_name}', ['_controller' => CategoryController::class]);
$starRoute = new Route('/star/{product_id}', ['_controller' => StarController::class]);
$promosRoute = new Route('/promos', ['_controller' => PromosController::class]);
$indexRoute = new Route('/', ['_controller' => IndexController::class]);
$routes = new RouteCollection();
$routes->add('product_show', $productRoute); public function registerBundles(): iterable
$routes->add('search_show', $searchRoute); {
$routes->add('category_show', $categoryRoute); return [
$routes->add('start_show', $starRoute); new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
$routes->add('promos_show', $promosRoute); new Symfony\Bundle\TwigBundle\TwigBundle()
$routes->add('index_show', $indexRoute); ];
}
$context = new RequestContext(); protected function configureContainer(ContainerConfigurator $container): void
$matcher = new UrlMatcher($routes, $context); {
try { $container->extension('framework', [
$parameters = $matcher->match($_SERVER['REQUEST_URI']); 'secret' => 'S0ME_SECRET'
} catch (Exception $e) { ]);
die($e->getMessage()); $container->services()
->load('Krzysiej\\RyobiCrawler\\Controller\\', __DIR__.'/src/Controller/*')
->autowire()
->autoconfigure()
;
}
protected function configureRoutes(RoutingConfigurator $routes): void
{
$routes->import(__DIR__.'/src/Controller/', 'attribute');
}
} }
match ($parameters['_controller']) { $kernel = new Kernel('prod', false);
SearchController::class => (new $parameters['_controller']())($parameters['search_term']), $request = Request::createFromGlobals();
CategoryController::class => (new $parameters['_controller']())($parameters['category_name']), $response = $kernel->handle($request);
ProductController::class, StarController::class => (new $parameters['_controller']())($parameters['product_id']), $response->send();
PromosController::class => (new $parameters['_controller']())(), $kernel->terminate($request, $response);
IndexController::class => (new $parameters['_controller']())(),
default => throw new Exception('Route not found')
};

48
index_old.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
include_once 'vendor/autoload.php';
use Krzysiej\RyobiCrawler\Controller\CategoryController;
use Krzysiej\RyobiCrawler\Controller\IndexController;
use Krzysiej\RyobiCrawler\Controller\ProductController;
use Krzysiej\RyobiCrawler\Controller\PromosController;
use Krzysiej\RyobiCrawler\Controller\SearchController;
use Krzysiej\RyobiCrawler\Controller\StarController;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
if (!file_exists('database.sqlite')) {
exit('Database file <code>database.sqlite</code> missing. Run docker compose <blockquote>docker compose exec php-app php index.php app:migrate</blockquote> to create it.');
}
$productRoute = new Route('/product/{product_id<\d+>}', ['_controller' => ProductController::class]);
$searchRoute = new Route('/?search={search_term}', ['_controller' => SearchController::class]);
$categoryRoute = new Route('/category/{category_name}', ['_controller' => CategoryController::class]);
$starRoute = new Route('/star/{product_id}', ['_controller' => StarController::class]);
$promosRoute = new Route('/promos', ['_controller' => PromosController::class]);
$indexRoute = new Route('/', ['_controller' => IndexController::class]);
$routes = new RouteCollection();
$routes->add('product_show', $productRoute);
$routes->add('search_show', $searchRoute);
$routes->add('category_show', $categoryRoute);
$routes->add('start_show', $starRoute);
$routes->add('promos_show', $promosRoute);
$routes->add('index_show', $indexRoute);
$context = new RequestContext();
$matcher = new UrlMatcher($routes, $context);
try {
$parameters = $matcher->match($_SERVER['REQUEST_URI']);
} catch (Exception $e) {
die($e->getMessage());
}
match ($parameters['_controller']) {
SearchController::class => (new $parameters['_controller']())($parameters['search_term']),
CategoryController::class => (new $parameters['_controller']())($parameters['category_name']),
ProductController::class, StarController::class => (new $parameters['_controller']())($parameters['product_id']),
PromosController::class, IndexController::class => (new $parameters['_controller']())(),
default => throw new Exception('Route not found')
};

View File

@@ -2,11 +2,11 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Twig\Environment; use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use Illuminate\Database\Capsule\Manager as Capsule; use Illuminate\Database\Capsule\Manager as Capsule;
class BaseController class BaseController extends AbstractController
{ {
protected Environment $twig; protected Environment $twig;
@@ -16,7 +16,5 @@ class BaseController
$capsule->addConnection(['driver' => 'sqlite', 'database' => __DIR__ . '/../../database.sqlite']); $capsule->addConnection(['driver' => 'sqlite', 'database' => __DIR__ . '/../../database.sqlite']);
$capsule->setAsGlobal(); $capsule->setAsGlobal();
$capsule->bootEloquent(); $capsule->bootEloquent();
$loader = new FilesystemLoader( __DIR__ . '/../../src/templates');
$this->twig = new Environment($loader);
} }
} }

View File

@@ -3,10 +3,13 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class CategoryController extends BaseController final class CategoryController extends BaseController
{ {
public function __invoke(string $category): void #[Route('/category/{category}', name: 'app_category')]
public function __invoke(string $category): Response
{ {
$products = Product::with('price') $products = Product::with('price')
->selectRaw('products.*') ->selectRaw('products.*')
@@ -15,6 +18,6 @@ final class CategoryController extends BaseController
->orderByDesc('starred') ->orderByDesc('starred')
->orderByDesc('created_by') ->orderByDesc('created_by')
->get(); ->get();
$this->twig->display('productList.html.twig', ['products' => $products]); return $this->render('productList.html.twig', ['products' => $products]);
} }
} }

View File

@@ -3,12 +3,15 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class IndexController extends BaseController final class IndexController extends BaseController
{ {
public function __invoke(): void #[Route('/', name: 'app_home')]
public function __invoke(): Response
{ {
$products = Product::with('price')->orderByDesc('starred')->orderByDesc('created_by')->get(); $products = Product::with('price')->orderByDesc('starred')->orderByDesc('created_by')->get();
$this->twig->display('productList.html.twig', ['products' => $products]); return $this->render('productList.html.twig', ['products' => $products]);
} }
} }

View File

@@ -4,6 +4,8 @@ namespace Krzysiej\RyobiCrawler\Controller;
use Illuminate\Support\ItemNotFoundException; use Illuminate\Support\ItemNotFoundException;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\RuntimeError; use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
@@ -15,18 +17,19 @@ final class ProductController extends BaseController
* @throws RuntimeError * @throws RuntimeError
* @throws LoaderError * @throws LoaderError
*/ */
public function __invoke(int $productId): void #[Route('/product/{productId<\d+>}', name: 'app_product')]
public function __invoke(int $productId): Response
{ {
$product = Product::with([ $product = Product::with([
'price' => fn($query) => $query->orderBy('created_at', 'desc') 'price' => fn($query) => $query->orderBy('created_at', 'desc')
])->find($productId); ])->find($productId);
if(null === $product) { if(null === $product) {
throw new ItemNotFoundException('Product not found'); throw $this->createNotFoundException('Product not found');
} }
$priceList = $product->price()->pluck('price')->implode(','); $priceList = $product->price()->pluck('price')->implode(',');
$priceDates = $product->price()->pluck('created_at')->map(fn($date) => $date->format('Y-m-d'))->implode("','"); $priceDates = $product->price()->pluck('created_at')->map(fn($date) => $date->format('Y-m-d'))->implode("','");
$this->twig->display('product.html.twig', ['product' => $product, 'price_list' => $priceList, 'price_dates' => $priceDates]); return $this->render('product.html.twig', ['product' => $product, 'price_list' => $priceList, 'price_dates' => $priceDates]);
} }
} }

View File

@@ -4,12 +4,15 @@ namespace Krzysiej\RyobiCrawler\Controller;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PromosController extends BaseController final class PromosController extends BaseController
{ {
public function __invoke(): void #[Route('/promos', name: 'app_promos')]
public function __invoke(): Response
{ {
$products = Product::whereHas('currentPrice', fn(Builder $query) => $query->whereColumn('price', '<', 'productStandardPrice'))->with(['currentPrice'])->get(); $products = Product::whereHas('currentPrice', fn(Builder $query) => $query->whereColumn('price', '<', 'productStandardPrice'))->with(['currentPrice'])->get();
$this->twig->display('productList.html.twig', ['products' => $products]); return $this->render('productList.html.twig', ['products' => $products]);
} }
} }

View File

@@ -3,15 +3,21 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class SearchController extends BaseController final class SearchController extends BaseController
{ {
public function __invoke(string $search): void #[Route('/search', name: 'app_search')]
public function __invoke(Request $request): Response
{ {
$search = $request->query->get('search');
//dd();
$products = Product::with('price') $products = Product::with('price')
->orWhere([['name', 'like', "%$search%"]]) ->orWhere([['name', 'like', "%$search%"]])
->orWhere([['subTitle', 'like', "%$search%"]])->get(); ->orWhere([['subTitle', 'like', "%$search%"]])->get();
$this->twig->display('productList.html.twig', ['products' => $products ?? [], 'search' => $search]); return $this->render('productList.html.twig', ['products' => $products ?? [], 'search' => $search]);
} }
} }

View File

@@ -3,12 +3,16 @@
namespace Krzysiej\RyobiCrawler\Controller; namespace Krzysiej\RyobiCrawler\Controller;
use Krzysiej\RyobiCrawler\Models\Product; use Krzysiej\RyobiCrawler\Models\Product;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class StarController extends BaseController final class StarController extends BaseController
{ {
public function __invoke(int $productId): void #[Route('/star/{productId<\d+>}', name: 'app_star')]
public function __invoke(int $productId, Request $request): Response
{ {
Product::find($productId)->toggleStarred()->save(); Product::find($productId)->toggleStarred()->save();
header('Location: ' . $_SERVER['HTTP_REFERER']); return $this->redirect($request->headers->get('referer'));
} }
} }

View File

@@ -18,7 +18,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property string $variantCode * @property string $variantCode
* @property string $modelCode * @property string $modelCode
* @property string $url * @property string $url
* @property boolean $starred * @property int $starred
*/ */
class Product extends Model class Product extends Model
{ {
@@ -30,10 +30,16 @@ class Product extends Model
return $this->hasMany(Price::class); return $this->hasMany(Price::class);
} }
public function isStarred(): bool
{
return (bool) $this->starred;
}
public function currentPrice(): HasOne public function currentPrice(): HasOne
{ {
return $this->hasOne(Price::class)->latestOfMany('created_at'); return $this->hasOne(Price::class)->latestOfMany('created_at');
} }
public function stock(): HasMany public function stock(): HasMany
{ {
return $this->hasMany(Stock::class); return $this->hasMany(Stock::class);
@@ -42,6 +48,7 @@ class Product extends Model
public function toggleStarred(): self public function toggleStarred(): self
{ {
$this->starred = !$this->starred; $this->starred = !$this->starred;
return $this; return $this;
} }

View File

@@ -3,14 +3,15 @@
{% block content %} {% block content %}
<table class='table table-hover'> <table class='table table-hover'>
<tr> <tr>
<td class="align-middle font-weight-bold h3"><a class="text-warning text-decoration-none" href="/star/{{ product.id }}">{% if product.starred %}{% else %}{% endif %}</a></td> <td class="align-middle font-weight-bold h3"><a class="text-warning text-decoration-none"
href="{{ path('app_star', {'productId': product.id}) }}">{% if product.starred %}{% else %}{% endif %}</a></td>
<td><img src='{{ product.image }}&width=150' class='border rounded p-1' alt='{{ product.name }}'/></td> <td><img src='{{ product.image }}&width=150' class='border rounded p-1' alt='{{ product.name }}'/></td>
<td><a href='/product/{{ product.id }}' class="text-decoration-none">{{ product.name }}</a></td> <td><a href='{{ path('app_home') }}/product/{{ product.id }}' class="text-decoration-none">{{ product.name }}</a></td>
<td>{{ product.subTitle }}</td> <td>{{ product.subTitle }}</td>
<td> <td>
<ul class='nav'> <ul class='nav'>
{% for category in product.categories %} {% for category in product.categories %}
<li class="nav-item"><a class="nav-link" href="/category/{{ category }}"> {{ category }} </a></li> <li class="nav-item"><a class="nav-link" href="{{ path('app_category', {'category': category}) }}"> {{ category }} </a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>

View File

@@ -15,14 +15,14 @@
</thead> </thead>
{% for product in products %} {% for product in products %}
<tr> <tr>
<td class="align-middle font-weight-bold h3"><a class="text-warning text-decoration-none" href="/star/{{ product.id }}">{% if product.starred %}{% else %}{% endif %}</a></td> <td class="align-middle font-weight-bold h3"><a class="text-warning text-decoration-none" href="{{ path('app_star', {'productId': product.id}) }}">{% if product.starred == true %}{% else %}{% endif %}</a></td>
<td class="align-middle" style="width: 120px;"><img src='{{ product.image }}&width=70' class='border rounded p-1' alt='{{ product.name }}'/></td> <td class="align-middle" style="width: 120px;"><img src='{{ product.image }}&width=70' class='border rounded p-1' alt='{{ product.name }}'/></td>
<td class="align-middle"><a href='/product/{{ product.id }}' class="text-decoration-none">{{ product.name }}</a></td> <td class="align-middle"><a href='{{ path('app_product', {'productId': product.id}) }}' class="text-decoration-none">{{ product.name }}</a></td>
<td class="align-middle">{{ product.subTitle }}</td> <td class="align-middle">{{ product.subTitle }}</td>
<td class="align-middle"> <td class="align-middle">
<ul class='nav'> <ul class='nav'>
{% for category in product.categories %} {% for category in product.categories %}
<li class="nav-item"><a class="nav-link" href="/category/{{ category }}"> {{ category }} </a></li> <li class="nav-item"><a class="nav-link" href="{{ path('app_category', {'category': category}) }}"> {{ category }} </a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>

View File

@@ -11,7 +11,7 @@
<body> <body>
<nav class="navbar navbar-expand-lg sticky-top bg-body-tertiary border-bottom border-secondary border-1"> <nav class="navbar navbar-expand-lg sticky-top bg-body-tertiary border-bottom border-secondary border-1">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/">Crawler</a> <a class="navbar-brand" href="{{ path('app_home') }}">Crawler</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@@ -19,14 +19,14 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/promos">Promos</a> <a class="nav-link active" aria-current="page" href="{{ path('app_promos') }}">Promos</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<form class="d-flex w-50" role="search" action="/"> <form class="d-flex w-50" role="search" action="{{ path('app_search') }}">
<input class="form-control me-2" type="search" name="search" placeholder="Search term eg. 36v or RCS18X" value="{{ search }}" aria-label="Search"> <input class="form-control me-2" type="search" name="search" placeholder="Search term eg. 36v or RCS18X" value="{{ search|default('') }}" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button> <button class="btn btn-outline-success" type="submit">Search</button>
</form> </form>
</nav> </nav>