diff --git a/docs/developer/src/routes_and_controllers.md b/docs/developer/src/routes_and_controllers.md index e69de29bb2..cc78397293 100644 --- a/docs/developer/src/routes_and_controllers.md +++ b/docs/developer/src/routes_and_controllers.md @@ -0,0 +1,96 @@ +# Routes and Controllers +## Routes +When GNU social receives a request, it calls a +controller to generate the response. The routing +configuration defines which action to run for each +incoming URL. + +You create routes by handling the `AddRoute` event. + +```php +public function onAddRoute(RouteLoader $r) +{ + $r->connect('avatar', '/{gsactor_id<\d+>}/avatar/{size?full}', + [Controller\Avatar::class, 'avatar_view']); + $r->connect('settings_avatar', '/settings/avatar', + [Controller\Avatar::class, 'settings_avatar']); + return Event::next; +} +``` + +The magic goes on `$r->connect((string $id, string $uri_path, array|string $target, array $param_reqs = [], array $accept_headers = [], array $options = []))`. +Here how it works: +* `id`: an identifier for your route so that you can easily refer to it later; +* `uri_path`: the url to be matched, can be static or have parameters; + The variable parts are wrapped in `{...}` and they must have a unique name; +* `target`: Can be an array _[Class, Method to invoke]_ or a string with _Class_ to __invoke; +* `param_reqs`: You can either do `['parameter_name' => 'regex']` or write the requirement inline `{parameter_name}`; +* `accept_headers`: If `[]` then the route will accept any [HTTP Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept). + If the route should only be used for certain Accept headers, then specify an array of the form: `['content type' => q-factor weighting]`; +* `options['format']`: Response content-type; +* `options['conditions']`: https://symfony.com/doc/current/routing.html#matching-expressions ; +* `options['template']`: Render a twig template directly from the route. + +### Observations + +* The special parameter `_format` can be used to set the "request format" of the Request object. This is used for such things as setting the Content-Type of the response (e.g. a json format translates into a Content-Type of application/json). + This does _not_ override the `options['format']` nor the `HTTP Accept header` information. +```php +$r->connect(id: 'article_show', uri_path: '/articles/search.{_format}', + target: [ArticleController::class, 'search'], + param_reqs: ['_format' => 'html|xml'] +); +``` +* An example of a suitable accept headers array would be: +```php +$acceptHeaders = [ + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0, + 'application/activity+json' => 1, + 'application/json' => 2, + 'application/ld+json' => 3 +]; +``` + +## Controllers + +A controller is a PHP function you create that reads information from the Request object and creates and returns a +either a Response object or an array that merges with the route `options` array. +The response could be an HTML page, JSON, XML, a file download, a redirect, a 404 error or anything else. + +### HTTP method + +```php +/** +* @param Request $request +* @param array $vars Twig Template vars and route options +*/ +public function onGet(Request $request, array $vars): array|Response +{ + return +} +``` + +### Forms + +```php +public function settings_avatar(Request $request): array +{ + $form = Form::create([ + ['avatar', FileType::class, ['label' => _m('Avatar'), 'help' => _m('You can upload your personal avatar. The maximum file size is 2MB.'), 'multiple' => false, 'required' => false]], + ['remove', CheckboxType::class, ['label' => _m('Remove avatar'), 'help' => _m('Remove your avatar and use the default one'), 'required' => false, 'value' => false]], + ['hidden', HiddenType::class, []], + ['save', SubmitType::class, ['label' => _m('Submit')]], + ]); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + $user = Common::user(); + $gsactor_id = $user->getId(); + // Do things + } + + return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()]; +} +``` \ No newline at end of file diff --git a/src/Core/Controller.php b/src/Core/Controller.php index 062ecc4830..9c1d126d86 100644 --- a/src/Core/Controller.php +++ b/src/Core/Controller.php @@ -32,6 +32,7 @@ namespace App\Core; use App\Util\Common; use App\Util\Exception\RedirectException; +use Exception; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -41,6 +42,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\KernelEvents; class Controller extends AbstractController implements EventSubscriberInterface @@ -98,7 +100,7 @@ class Controller extends AbstractController implements EventSubscriberInterface $template = $this->vars['_template']; unset($this->vars['_template'], $this->vars['request']); - // Respond in the the most preffered acceptable content type + // Respond in the most preferred acceptable content type $format = $request->getFormat($request->getAcceptableContentTypes()[0]); switch ($format) { case 'html': @@ -159,7 +161,7 @@ class Controller extends AbstractController implements EventSubscriberInterface return (bool) $value; default: Log::critical($m = "Method '{$method}' on class App\\Core\\Controller not found (__call)"); - throw new \Exception($m); + throw new Exception($m); } } }