ldc-boutique-cnc/app/Jobs/RefreshSiteMap.php
2022-03-24 19:38:51 +01:00

407 lines
13 KiB
PHP

<?php
namespace App\Jobs;
use App\Models\Range;
use App\Models\Tag;
use App\Models\Product;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Routing\Route as RoutingRoute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Kwaadpepper\Enum\BaseEnum;
use ReflectionClass;
use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;
class RefreshSiteMap implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Uri that start with something in
* this table will be ignored
*
* @var array
*/
protected $ignoreRoutes = [
'/login',
'/bo',
'/activer',
// '/deconnexion',
'/reinitialiser-mdp',
// '/le-club-des-gaulois/la-boutique',
// '/password/confirm',
// '/produit/from-menus',
// '/cote-cuisine/sante-et-nutrition/menu-equilibre-j',
// '/la-marque/tracabilite/search',
// '/frame',
// '/map',
'/_debugbar', // Hack ignore laravel debug bar
'_ignition/health-check', //ignore ignition route
'_ignition/scripts/{script}', //ignore ignition route
'_ignition/styles/{style}' //ignore ignition route
];
/**
* liste des routes à binder
*
* @var array
*/
protected $routesBinder = [
'range' => [
'rangeSlug' => [Range::class, 'slug']
],
'products.tagfilter' => [
'tagSlug' => [Tag::class, 'slug']
],
'product' => [
'productSlug' => [Product::class, 'slug']
],
];
/**
* Execute the job.
*
*! Ici c'est du sale !!
* TODO: mettre tout ça dans un package pour le rendre plus générique
*
* @return void
*/
public function handle()
{
$this->assertRouteBinderIsCorrect();
$siteMap = [];
$routeCollection = Route::getRoutes();
/** @var \Illuminate\Routing\Route $route */
foreach ($routeCollection as $route) {
if ($this->routeShouldBeIgnored($route)) {
continue;
}
if (!$this->routeHandlesGetMethod($route)) {
continue;
}
/** @var array to be populated with models from DB to generate a route */
$rParams = [];
/** @var \array [lluminate\Routing\Route::signatureParameters] parameters signature from the controller */
$params = $route->signatureParameters();
/** @var \array [Illuminate\Routing\Route::parameterNames] parameters name from the registered route */
$pNames = $route->parameterNames();
$hasModels = false;
foreach ($params as $param) {
/** @var ReflectionClass|null $name */
$name = $param->getType() && !$param->getType()->isBuiltin()
? new ReflectionClass($param->getType()->getName())
: null;
/** @var string $pName $parameter name on route uri */
$pName = '';
// If the route has parameters and the controller accepts it
if (count($pNames) and \array_key_exists($param->getPosition(), $pNames)) {
// Get the controller parameter name for the route
$pName = $pNames[$param->getPosition()];
}
// If We have a custom binder for this route
if ($this->hasRouteBinderParam($route, $pName)) {
$hasModels = true;
$this->processWithRouteBinderParam($rParams, $route, $pName);
continue;
}
// If This route takes a model as param
if ($name and $name->isSubclassOf(Model::class)) {
$hasModels = true;
/** @var \string */
$modelClassName = $name->getName();
$this->setParamsForModel($rParams, $modelClassName, $pName);
continue;
}
}
// If the route has no dynamic models as parameters
if (!count($params) or !$hasModels) {
$siteMap[] = [
route($route->getName()),
Url::CHANGE_FREQUENCY_MONTHLY,
0.1
];
} else { // If the route has dynamic models as parameters
$this->genRoute($route, $siteMap, $rParams);
}
}
$sm = Sitemap::create();
foreach ($siteMap as $smRules) {
$sm->add(Url::create($smRules[0])
->setLastModificationDate(Carbon::yesterday())
->setChangeFrequency($smRules[1])
->setPriority($smRules[2]));
}
$sm->writeToFile(\public_path('sitemap.xml'));
}
/**
* Process route with binderParam
*
* @param RoutingRoute $route
* @param string $pName
* @return void
*/
private function processWithRouteBinderParam(array &$rParams, RoutingRoute $route, string $pName)
{
$o = $this->getRouteBinderParam($route, $pName);
/** @var \string */
$modelClassName = $o[0];
/** @var \string */
$routeParamName = $o[1];
$rfLCls = (new ReflectionClass($modelClassName));
switch (true) {
case $rfLCls->isSubclassOf(Model::class):
$this->setParamsForModel($rParams, $modelClassName, $pName, $routeParamName);
break;
case $rfLCls->isSubclassOf(BaseEnum::class):
$this->setParamForEnum($rParams, $rfLCls->getName(), $pName);
break;
default:
throw new Exception(sprintf('Unhandled class type %s', $rfLCls->getName()));
}
}
/**
* Set route param Array for Enums
*
* @param array $rParams
* @param string $enumClassName
* @param string $pName
* @return void
*/
private function setParamForEnum(array &$rParams, string $enumClassName, string $pName)
{
foreach (forward_static_call(sprintf('%s::toArray', $enumClassName)) as $enum) {
if (!array_key_exists($pName, $rParams)) {
$rParams[$pName] = [];
}
$rParams[$pName][] = $enum->value;
}
}
/**
* Set route param Array for Models
*
* @param array $rParams
* @param string $modelClassName
* @param string $pName
* @param string $routeParamName
* @return void
*/
private function setParamsForModel(
array &$rParams,
string $modelClassName,
string $pName,
string $routeParamName = null
) {
$i = 0;
$limit = 10;
$modelTable = (new ReflectionClass($modelClassName))->newInstanceWithoutConstructor()->getTable();
$q = $modelClassName::query();
$q = Schema::hasColumn($modelTable, 'status') ? $q->where('status', true) : $q;
$q = Schema::hasColumn($modelTable, 'ocbStatus') ? $q->where('ocbStatus', EleveurOcbStatus::yes()->value) : $q;
$q = Schema::hasColumn($modelTable, 'star_moment') ? $q->where('star_moment', true) : $q;
do {
$results = $q->offset($i * $limit)->limit($limit)->get();
$length = count($results);
$results->map(function (Model $model) use (&$rParams, $pName, $routeParamName) {
if (!array_key_exists($pName, $rParams)) {
$rParams[$pName] = [];
}
if ($routeParamName) {
$rParams[$pName][] = $model[$routeParamName];
} else {
$rParams[$pName][] = $model;
}
});
$i++;
} while ($length);
}
/**
* Recursively generate routes for all params
*
* @param RoutingRoute $route
* @param array $siteMap
* @param array $params
* @return void
*/
private function genRoute(RoutingRoute $route, array &$siteMap, array $params)
{
$filtered = false;
foreach ($params as $pName => $param) {
if (is_array($param)) {
collect($param)->map(function ($p) use ($route, &$siteMap, $params, $pName) {
$this->genRoute($route, $siteMap, array_merge($params, [$pName => $p]));
});
return;
}
$filtered = array_key_last($params) === $pName ? true : false;
}
if ($filtered) {
$siteMap[] = [
route($route->getName(), $params),
Url::CHANGE_FREQUENCY_DAILY,
0.1
];
}
}
/**
* Check if $route exists in routesBinder
*
* @param RoutingRoute $route
* @return boolean
*/
private function hasRouteBinder(RoutingRoute $route): bool
{
return array_key_exists($route->getName(), $this->routesBinder);
}
/**
* Check if $route exists in routesBinder and has $param
*
* @param RoutingRoute $route
* @param string $param
* @return boolean
*/
private function hasRouteBinderParam(RoutingRoute $route, string $param): bool
{
if ($this->hasRouteBinder($route)) {
return array_key_exists($param, $this->getRouteBinder($route));
}
return false;
}
/**
* Get the routeBinder
*
* @param RoutingRoute $route
* @return array
*/
private function getRouteBinder(RoutingRoute $route): array
{
return $this->routesBinder[$route->getName()];
}
/**
* Get the routeBinder param
*
* @param RoutingRoute $route
* @param string $param
* @return array
*/
private function getRouteBinderParam(RoutingRoute $route, string $param): array
{
return $this->routesBinder[$route->getName()][$param];
}
/**
* Assert that routeBinder is correct
*
* @throws Exception
* @return void
*/
private function assertRouteBinderIsCorrect()
{
/** @var \Illuminate\Routing\RouteCollection */
$routeCollection = Route::getRoutes();
foreach ($this->routesBinder as $routeName => $params) {
if (!$routeCollection->hasNamedRoute($routeName)) {
throw new Exception(sprintf('Route %s does not exists', $routeName));
}
foreach ($params as $pName => $p) {
if (count($p) != 2) {
throw new Exception(sprintf(
'Route %s parameter %s does not have Model classname and parameter name',
$routeName,
$pName
));
}
$rfCls = (new ReflectionClass($p[0]))->newInstanceWithoutConstructor();
if (is_subclass_of($rfCls, BaseEnum::class)) {
return;
}
if (!is_subclass_of($rfCls, Model::class)) {
throw new Exception(sprintf(
'Route "%s" parameter "%s" first element must be a child of "%s"',
$routeName,
$pName,
Model::class
));
}
if (!Schema::hasColumn($rfCls->getTable(), $p[1])) {
throw new Exception(sprintf(
'"%s" parameter "%s" second element "%s" must refer to and existing database column of "%s"',
$routeName,
$pName,
$p[1],
$p[0]
));
}
}
}
}
/**
* Check if the route handles the HTTP GET method
*
* @param RoutingRoute $route
* @return boolean
*/
private function routeHandlesGetMethod(RoutingRoute $route): bool
{
return in_array('GET', $route->methods());
}
/**
* Check if the actual route uri start with
* a partial uri from the table
* Check also if it uses middleware auth
*
* @param RoutingRoute $route
* @return boolean
*/
private function routeShouldBeIgnored(RoutingRoute $route): bool
{
foreach ($this->ignoreRoutes as $iRoute) {
if (strpos($route->uri(), ltrim($iRoute, '/')) === 0) {
return true;
}
}
foreach ($route->middleware() as $middleware) {
if (strpos($middleware, 'auth') === 0) {
return true;
}
}
return false;
}
}