I have a small Symfony project I want to document with doxygen. There are two .php files that should be included. One is documented, the other is not, and I cannot figure out why that may be.
Folder structure is:
project
└--src
|--Controller
| └--FormController.php
└--Model
└--Inquiry.php
Doxygen is reading and parsing both files...
Reading /form-handler/src/Controller/FormController.php...
Parsing file /form-handler/src/Controller/FormController.php...
Reading /form-handler/src/Model/Inquiry.php...
Parsing file /form-handler/src/Model/Inquiry.php...
...but only documents FormController.php
, not Inquiry.php
:
Generating docs for compound App::Controller::FormController...
For some reason doxygen does not seem to recognizeInquiry.php
as a class.
What I have tried:
- Removed decorators from docstrings that might offend doxygen.
- Checked format of docstrings
- Enabled/disabled various Doxyfile options
FormController.php:
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Model\Inquiry;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* Handles incoming requests.
*/
class FormController extends AbstractController
{
/**
* Handles POST requests.
*
* @return Response contains JSON object with 'message' value
*/
#[Route('/', methods: ['POST'])]
public function handleRequest(
HttpClientInterface $client,
Inquiry $inquiry,
LoggerInterface $logger,
RateLimiterFactory $formApiLimiter,
Request $request,
): Response {
$logger->debug('Received a POST request');
// set up a rate limiter by IP
// rules are defined in /config/packages/rate-limiter.yaml
$limiter = $formApiLimiter->create($request->getClientIp());
$limit = $limiter->consume();
// configure headers exposing rate limit info
$headers = [
'Content-Type' => 'application/json',
'X-RateLimit-Remaining' => $limit->getRemainingTokens(),
'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp(),
'X-RateLimit-Limit' => $limit->getLimit(),
];
if (false === $limit->isAccepted()) {
return new Response(
content: json_encode(['message' => null]),
status: Response::HTTP_TOO_MANY_REQUESTS,
headers: $headers
);
}
// make sure all required fields are included in request and not empty
$requiredFields = ['subject', 'message', 'consent', 'h-captcha-response'];
$providedFields = $request->request->keys();
foreach ($requiredFields as $field) {
if (!in_array($field, $providedFields)) {
return new Response(
content: json_encode(['message' => "Pflichtfeld '".$field."' fehlt."]),
status: Response::HTTP_BAD_REQUEST,
headers: $headers
);
} elseif ('' == filter_var($request->request->get($field), FILTER_SANITIZE_SPECIAL_CHARS)) {
return new Response(
content: json_encode(['message' => "Pflichtfeld '".$field."' darf nicht leer sein."]),
status: Response::HTTP_BAD_REQUEST,
headers: $headers
);
}
}
// verify captcha success
$captcha = filter_var($request->request->get('h-captcha-response'), FILTER_SANITIZE_SPECIAL_CHARS);
$data = [
'secret' => $this->getParameter('app.captcha.secret'),
'response' => $captcha,
];
try {
$hCaptchaResponse = $client->request(
method: 'POST',
url: 'https://hcaptcha.com/siteverify',
options: [
'body' => $data,
],
);
$hCaptchaResponseJson = json_decode($hCaptchaResponse->getContent(), true);
if (!$hCaptchaResponseJson['success']) {
return new Response(
content: json_encode(['message' => 'Captcha fehlgeschlagen']),
status: Response::HTTP_BAD_REQUEST,
headers: $headers
);
}
// exceptions on the side of hCaptcha are logged, but the request is processed anyway
} catch (TransportExceptionInterface $e) {
$logger->debug('Could not reach hCaptcha verification server: '.$e);
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface $e) {
$logger->debug('Error when verifying hCaptcha response: '.$e);
}
// get values from request data
$name = filter_var($request->request->get('name'), FILTER_SANITIZE_SPECIAL_CHARS);
$email = filter_var($request->request->get('email'), FILTER_SANITIZE_EMAIL);
$phone = filter_var($request->request->get('phone'), FILTER_SANITIZE_SPECIAL_CHARS);
$subject = filter_var($request->request->get('subject'), FILTER_SANITIZE_SPECIAL_CHARS);
$message = filter_var($request->request->get('message'), FILTER_SANITIZE_SPECIAL_CHARS);
$consent = filter_var($request->request->get('consent'), FILTER_SANITIZE_SPECIAL_CHARS);
// translate into a boolean (else the string 'false' will be evaluated as true)
$consent = filter_var($consent, FILTER_VALIDATE_BOOLEAN);
// populate Inquiry with request data
$inquiry->createInquiry(
subject: $subject,
message: $message,
consent: $consent,
name: $name,
email: $email,
phone: $phone,
);
// validate Inquiry
$validationResult = $inquiry->validateInquiry();
// if Inquiry is invalid, return validation violation message(s)
if (count($validationResult) > 0) {
$logger->debug($validationResult);
// assemble list of error messages
$validationMessages = [];
foreach ($validationResult as $result) {
$validationMessages = [$result->getPropertyPath() => $result->getMessage()];
}
return new Response(
content: json_encode([
'message' => 'Anfrage enthält ungültige Werte',
'errors' => $validationMessages,
]),
status: Response::HTTP_BAD_REQUEST,
headers: $headers
);
}
// send mail to office
$emailResult = $inquiry->sendOfficeEmail();
$logger->debug(implode(' ,', $emailResult));
$message = 'Die Anfrage war erfolgreich';
if (!$emailResult['success']) {
$message = 'Die Anfrage war nicht erfolgreich.';
}
// TODO compile email to user
$data = [
'message' => $message,
'officeEmail' => $emailResult,
'confirmationEmail' => true,
];
return new Response(
content: json_encode($data),
status: Response::HTTP_OK,
headers: $headers
);
}
/**
* Handles disallowed request methods.
*
* @return Response contains JSON object with 'message' value
*/
#[Route('/', condition: "context.getMethod() not in ['POST']")]
public function handleDisallowedMethods(LoggerInterface $logger): Response
{
$logger->debug('Received a request with a disallowed method.');
return new Response(
content: json_encode(['message' => 'Only POST requests allowed']),
status: Response::HTTP_METHOD_NOT_ALLOWED
);
}
}
Inquiry.php:
<?php
declare(strict_types=1);
namespace App\Model;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* Represents an inquiry received from the front end.
*
* The required fields 'subject', 'message', and
* 'consent' must be provided to the constructor.
*/
class Inquiry
{
private string $name;
private string $email;
private string $phone;
private string $subject;
private string $message;
private bool $consent;
private bool $validated = false;
/**
* @param LoggerInterface $logger
* @param MailerInterface $mailer
* @param array $officeRecipients configured in services.yaml
* @param ValidatorInterface $validator
*/
public function __construct(
private readonly LoggerInterface $logger,
private readonly MailerInterface $mailer,
private readonly array $officeRecipients,
private readonly ValidatorInterface $validator,
) {
}
/**
* Populates an inquiry with data.
*
* 'subject', 'message', and 'consent are required,
* all other values are optional.
*
* Sets 'validated' to false, in case createInquiry()
* is called multiple times.
*/
public function createInquiry(
string $subject,
string $message,
bool $consent,
string $name = '',
string $email = '',
string $phone = '',
): void {
$this->subject = $subject;
$this->message = $message;
$this->consent = $consent;
$this->name = $name;
$this->email = $email;
$this->phone = $phone;
$this->validated = false;
}
/**
* Validates the inquiry.
*
* If successful, sets 'validated' to true
*
* @return ConstraintViolationListInterface if valid: empty
* if not valid: list of validation violation messages
*/
public function validateInquiry(): ConstraintViolationListInterface
{
$validationResult = $this->validator->validate($this);
if (0 == count($validationResult)) {
$this->validated = true;
}
return $validationResult;
}
/**
* Sends an email with the customer inquiry data to the office.
*
* @return array containing 'success' boolean and 'message' string
*/
public function sendOfficeEmail(): array
{
if (!$this->validated) {
return [
'success' => false,
'message' => 'Inquiry has not been validated. Use Inquiry->validate() first',
];
}
// convert 'consent' and empty values in to human-readable format
$plainTextConsent = $this->consent ? 'Ja' : 'Nein';
$plainTextName = $this->name ?: 'Keine Angabe';
$plainTextEmail = $this->email ?: 'Keine Angabe';
$plainTextPhone = $this->phone ?: 'Keine Angabe';
$emailBody = <<<END
Das Kontaktformular hat eine Anfrage erhalten.
Betreff: $this->subject
Nachricht: $this->message
Einwilligung: $plainTextConsent
Name: $plainTextName
Email: $plainTextEmail
Telefon: $plainTextPhone
END;
$email = (new Email())
->to(...$this->officeRecipients)
->subject('Anfrage vom Kontaktformular')
->text($emailBody);
try {
$this->mailer->send($email);
$this->logger->debug('Email sent');
return [
'success' => true,
'message' => 'Email wurde gesendet',
];
} catch (TransportExceptionInterface $e) {
$this->logger->debug('Error sending email: '.$e);
return [
'success' => false,
'message' => 'Email konnte nicht gesendet werden: '.$e,
];
}
}
/**
* @codeCoverageIgnore
*/
public function sendConfirmationEmail(): string
{
return '';
}
/**
* Checks whether Inquiry has been validated.
*/
public function isValidated(): bool
{
return $this->validated;
}
}
Doxyfile (EDITED as per @albert's comment):
# Difference with default Doxyfile 1.9.3 (c0b9eafbfb53286ce31e75e2b6c976ee4d345473)
PROJECT_NAME = "Form handler"
PROJECT_BRIEF = "Stand-alone Symfony backend to handle contact forms."
OUTPUT_DIRECTORY = ./doc/
INPUT = ./src/ \
README.md
RECURSIVE = YES
EXCLUDE = ./src/Kernel.php
USE_MDFILE_AS_MAINPAGE = README.md
GENERATE_LATEX = NO
CodePudding user response:
As of PHP version 7.3.0 the syntax of the here document changed slightly, see https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc
The closing identifier may be indented by space or tab, in which case the indentation will be stripped from all lines in the doc string. Prior to PHP 7.3.0, the closing identifier must begin in the first column of the line.
This has now been corrected in the proposed patch, pull request: github.com/doxygen/doxygen/pull/9398
Workarounds:
- place the end identifier of the here document at the beginning of the line
- place a doxygen conditional block /** \cond / / /* \endcond */ around the here document.