I am using API Platform 2.6
As I need to export some contents to PDF, I had to create a custom encoder for that to work.
1st, I registered a new format, so I can request application/pdf content.
#File: config/packages/api_platform:
api_platform:
....
formats:
....
pdf: ['application/pdf']
2nd, Created a PdfEncoder that extends EncoderInterface
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
public const FORMAT = 'pdf';
public function __construct(
private readonly Environment $twig,
private readonly Pdf $pdf,
) {
}
public function encode($data, $format, array $context = []): string
{
$content = $this->twig->render('pdf/template.html.twig', ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
public function supportsEncoding($format): bool
{
return self::FORMAT === $format;
}
}
3d, on the resource, I created the appropriate call:
#File src/Resource/MyResource.php
#[ApiResource(
itemOperations: [
'export' => [
'method' => 'GET',
'path' => '/myResource/{id}/export',
'requirements' => ['id' => '\d+'],
'output' => MyResourceView::class
],
],
)]
class MyResource
{
public int $id;
/** Other Attributes **/
}
As you can see, On the PdfEncoder class, I hard coded the path to the Twig Template,
But As I need to export other resources to PDF, and they are using different templates, I need to pass this template path as an option to the encoder, Maybe on the context variable would be great.
Here is what I am looking for.
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
....
public function encode($data, $format, array $context = []): string
{
$template = $context['export']['template']?? null;
if (!$template){
throw new \Exception('Twig template is not defined');
}
$content = $this->twig->render($template, ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
...
}
Is there a way to accomplish this?
I tried adding that on the Resource, but ApiPlatform deleted them before having them on the context array
#File src/Resource/MyResource.php
#[ApiResource(
itemOperations: [
'export' => [
'method' => 'GET',
'path' => '/myResource/{id}/export',
'requirements' => ['id' => '\d+'],
'output' => MyResourceView::class,
'export' => [
'template' => 'pdf/template.html.twig',
]
],
],
)]
class MyResource
{
public int $id;
/** Other Attributes **/
}
This is what I've come up with so far,
This helps me to get the template name dynamically.
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
public const FORMAT = 'pdf';
public function __construct(
private readonly Environment $twig,
private readonly Pdf $pdf,
) {
}
public function encode($data, $format, array $context = []): string
{
$template = sprintf('pdf/%s.html.twig', lcfirst($context['output']['name']));
if (!$this->twig->getLoader()->exists($template)) {
throw new \RuntimeException(sprintf('Missing template for %s', $context['output']['class']));
}
$content = $this->twig->render($template, ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
public function supportsEncoding($format): bool
{
return self::FORMAT === $format;
}
}
$context['output']['class'] is the name of the View class (base name)
So this way, for every View, I have a twig file.
I won't mark this answer as the solution as I think there might be nicer/recommended way to do that.
I am trying to configure my authentication with the new security system.
I would like the password verification to be done on a local sql or ldap.
With my current config, the check is done on the local sql, then on the ldap
how to make it one or the other ?
I have been going around in circles for several days...
My security.php
$containerConfigurator->extension('security', [
'providers' => [
'mercredi_user_provider' => [
'entity' => [
'class' => User::class,
'property' => 'username',
],
],
'ville_ldap' => [
'ldap' => [
'service' => 'Symfony\Component\Ldap\Ldap',
'base_dn' => '%env(ACLDAP_DN)%',
'search_dn' => '%env(ACLDAP_USER)%',
'search_password' => '%env(ACLDAP_PASSWORD)%',
'default_roles' => 'ROLE_BOTTIN_ADMIN',
'uid_key' => 'sAMAccountName',
'extra_fields' => ['mail'],
],
],
'all_users' => [
'chain' => [
'providers' => ['ville_ldap', 'mercredi_user_provider'],
],
],
],
]
);
$containerConfigurator->extension(
'security',
[
'firewalls' => [
'main' => [
'provider' => 'all_users',
'custom_authenticator' => MercrediAuthenticator::class,
'form_login_ldap' => [
'service' => 'Symfony\Component\Ldap\Ldap',
'search_dn' => '%env(ACLDAP_USER)%',
'search_password' => '%env(ACLDAP_PASSWORD)%',
'query_string' => '(&(|(sAMAccountName={username}))(objectClass=person))',
'dn_string' => '%env(ACLDAP_DN)%',
'check_path' => 'app_login',
'username_parameter' => 'username',
'password_parameter' => 'password',
],
'logout' => ['path' => 'app_logout'],
],
],
]
);
My Authenticator
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('username', '');
$password = $request->request->get('password', '');
$token = $request->request->get('_csrf_token', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
$badges =
[
new CsrfTokenBadge('authenticate', $token),
new PasswordUpgradeBadge($password, $this->userRepository),
new LdapBadge(LdapMercredi::class, $email),
];
return new Passport(
new UserBadge($email),
new PasswordCredentials($password), $badges
);
}
My class ldap
class LdapMercredi implements LdapInterface
{
private Ldap $ldap;
private string $dn;
private string $user;
private string $password;
public function __construct(string $host, string $dn, string $user, string $password)
{
$this->ldap = Ldap::create(
'ext_ldap',
[
'host' => $host,
'encryption' => 'ssl',
]
);
$this->user = $user;
$this->password = $password;
$this->dn = $dn;
}
public function getEntry(string $uid): ?Entry
{
$this->ldap->bind($this->user, $this->password);
$filter = "(&(|(sAMAccountName=*$uid*))(objectClass=person))";
$query = $this->ldap->query($this->dn, $filter, ['maxItems' => 1]);
$results = $query->execute();
if ($results->count() > 0) {
return $results[0];
}
return null;
}
public function getEntryManager(): EntryManagerInterface
{
return $this->ldap->getEntryManager();
}
public function bind(string $dn = null, string $password = null)
{
dd($dn);
// TODO: Implement bind() method.
}
public function query(string $dn, string $query, array $options = [])
{
dd($query);
// TODO: Implement query() method.
}
public function escape(string $subject, string $ignore = '', int $flags = 0)
{
// TODO: Implement escape() method.
}
}
UPDATE 23/08/21
Here is a configuration that works, there are some points that could improve.
For example, for the LdapBadge badge ('ldapServiceId') you have to give an ldap service, there is indeed the Symfony\Component\Ldap\Ldap which could be suitable but even if I add a tag 'ldap' in services.php
//services.php
$services->set(Ldap::class)->args(['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'])->tag('ldap');
I have the error message: "Cannot check credentials using the "Symfony\Component\Ldap\Ldap" ldap service, as such service is not found."
So I copied and pasted the code from Ldap.php to LdapMercredi and declare as service ldap:
In my services.php,
if (interface_exists(LdapInterface::class)) {
$services
->set(Symfony\Component\Ldap\Ldap::class)
->args(['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'])
->tag('ldap');
$services->set(Adapter::class)->args([
'$arguments' => [
'$host' => '%env(ACLDAP_URL)%',
'$port' => 636,
'$encryption' => 'ssl',
'$options' => [
'$protocole_version' => 3,
'$referrals' => false,
],
],
]);
$services->set(LdapMercredi::class)
->arg('$adapter', service('Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'))
->tag('ldap');
/**
* Copy/Paste
* #see Ldap
*/
class LdapMercredi implements LdapInterface
{
private $adapter;
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
/**
* {#inheritdoc}
*/
public function bind(string $dn = null, string $password = null)
{
$this->adapter->getConnection()->bind($dn, $password);
}
/**
* {#inheritdoc}
*/
public function query(string $dn, string $query, array $options = []): QueryInterface
{
return $this->adapter->createQuery($dn, $query, $options);
}
/**
* {#inheritdoc}
*/
public function getEntryManager(): EntryManagerInterface
{
return $this->adapter->getEntryManager();
}
/**
* {#inheritdoc}
*/
public function escape(string $subject, string $ignore = '', int $flags = 0): string
{
return $this->adapter->escape($subject, $ignore, $flags);
}
/**
* Creates a new Ldap instance.
*
* #param string $adapter The adapter name
* #param array $config The adapter's configuration
*
* #return static
*/
public static function create(string $adapter, array $config = []): self
{
if ('ext_ldap' !== $adapter) {
throw new DriverNotFoundException(
sprintf('Adapter "%s" not found. Only "ext_ldap" is supported at the moment.', $adapter)
);
}
return new self(new Adapter($config));
}
}
In security.php:
$containerConfigurator->extension('security', [
'encoders' => [
User::class => ['algorithm' => 'auto'],
],
]);
$containerConfigurator->extension('security', [
'providers' => [
'mercredi_user_provider' => [
'entity' => [
'class' => User::class,
'property' => 'username',
],
],
],
]
);
$authenticators = [MercrediAuthenticator::class];
$main = [
'provider' => 'mercredi_user_provider',
'logout' => ['path' => 'app_logout'],
'form_login' => [],
'entry_point' => MercrediAuthenticator::class,
];
if (interface_exists(LdapInterface::class)) {
$authenticators[] = MercrediLdapAuthenticator::class;
$main['form_login_ldap'] = [
'service' => 'Symfony\Component\Ldap\Ldap',
'check_path' => 'app_login',
];
}
$main['custom_authenticator'] = $authenticators;
$containerConfigurator->extension(
'security',
[
'firewalls' => [
'main' => $main,
],
]
);
MercrediAuthenticator.php
class MercrediAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
private UserRepository $userRepository;
private ParameterBagInterface $parameterBag;
public function __construct(
UrlGeneratorInterface $urlGenerator,
UserRepository $userRepository,
ParameterBagInterface $parameterBag
) {
$this->urlGenerator = $urlGenerator;
$this->userRepository = $userRepository;
$this->parameterBag = $parameterBag;
}
public function supports(Request $request): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getPathInfo();
}
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('username', '');
$password = $request->request->get('password', '');
$token = $request->request->get('_csrf_token', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
$badges =
[
new CsrfTokenBadge('authenticate', $token),
new PasswordUpgradeBadge($password, $this->userRepository),
];
return new Passport(
new UserBadge($email),
new PasswordCredentials($password), $badges
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('mercredi_front_profile_redirect'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
/**
* Override to change what happens after a bad username/password is submitted.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if ($request->hasSession()) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
}
if (interface_exists(LdapInterface::class)) {
return null;
}
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
public function isInteractive(): bool
{
return true;
}
The trick of authenticating with ldap and local sql is that the credentials verification priority is higher
Symfony\Component\Ldap\Security\CheckLdapCredentialsListener
class MercrediLdapAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
private UserRepository $userRepository;
private ParameterBagInterface $parameterBag;
public function __construct(
UrlGeneratorInterface $urlGenerator,
UserRepository $userRepository,
ParameterBagInterface $parameterBag
) {
$this->urlGenerator = $urlGenerator;
$this->userRepository = $userRepository;
$this->parameterBag = $parameterBag;
}
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('username', '');
$password = $request->request->get('password', '');
$token = $request->request->get('_csrf_token', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
$badges =
[
new CsrfTokenBadge('authenticate', $token),
new PasswordUpgradeBadge($password, $this->userRepository),//SelfValidatingPassport?
];
$query = "(&(|(sAMAccountName=*$email*))(objectClass=person))";
$badges[] = new LdapBadge(
LdapMercredi::class,
$this->parameterBag->get(Option::LDAP_DN),
$this->parameterBag->get(Option::LDAP_USER),
$this->parameterBag->get(Option::LDAP_PASSWORD),
$query
);
return new Passport(
new UserBadge($email),
new PasswordCredentials($password), $badges
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('mercredi_front_profile_redirect'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
In the docs there is this example, but it shows only how to add one GET operation.
I would like to know how can I add a custom POST route to the documentation.
I am having trouble to show the example body request, with the expected values to be sent (username and email, in this example)
My attempt
<?php
// api/src/Swagger/SwaggerDecorator.php
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decorated->normalize($object, $format, $context);
$customDefinition = [
'tags' => [
'default'
],
'name' => 'fields',
'description' => 'Testing decorator',
'default' => 'id',
'in' => 'query',
'requestBody' =>
[
'content' => [
'application/json' => [
'schema' => [
'description' => 'abcd',
'required' => [
'username', 'email'
],
'properties' => [
'username', 'email'
],
]
]
],
'description' => 'testing'
],
];
$docs['paths']['/testing']['post']['parameters'][] = $customDefinition;
return $docs;
}
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
}
But it doesn't work.
you should not put the whole route declaration inside the parameters array, you should create smth like this:
$docs['paths']['/testing']['post'] = $customDefinition;
Try add new module? but got are error:
Argument 1 passed to Post\Controller\PostController::__construct()
must be an instance of Post\Model\PostTable, none given, called in
W:\domains\zend_blog\skeleton-application\vendor\zendframework\zend-servicemanager\src\Factory\InvokableFactory.php
namespace Post\Controller;
use Post\Model\Post;
// Add the following import:
use Post\Model\PostTable;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class PostController extends AbstractActionController
{
private $table;
/**
* Execute the request
*
* #param MvcEvent $e
* #return mixed
*/
// Add this constructor:
public function __construct(PostTable $table)
{
$this->table = $table;
}
public function indexAction()
{
return new ViewModel([
'post' => $this->table->fetchAll()
]);
}
}
Post Table:
<?php
namespace Post\Model;
use RuntimeException;
use Zend\Db\TableGateway\TableGatewayInterface;
use Zend\Db\TableGateway\TableGateway;
class PostTable
{
private $tableGateway;
public function __construct(TableGateway $tableGateway)
{
// $this->tableGateway = $tableGateway;
$this->tableGateway = $tableGateway;
}
public function fetchAll()
{
return $this->tableGateway->select();
}
public function getPost($id)
{
$id = (int) $id;
$rowset = $this->tableGateway->select(['id' => $id]);
$row = $rowset->current();
if (! $row) {
throw new RuntimeException(sprintf(
'Could not find row with identifier %d',
$id
));
}
return $row;
}
public function savePodt(Post $album)
{
$data = [
'artist' => $album->artist,
'title' => $album->title,
];
$id = (int) $album->id;
if ($id === 0) {
$this->tableGateway->insert($data);
return;
}
if (! $this->getPost($id)) {
throw new RuntimeException(sprintf(
'Cannot update album with identifier %d; does not exist',
$id
));
}
$this->tableGateway->update($data, ['id' => $id]);
}
public function deletePost($id)
{
$this->tableGateway->delete(['id' => (int) $id]);
}
}
Module:
namespace Post;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
class Module implements ConfigProviderInterface,ServiceProviderInterface
{
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
/**
* Expected to return \Zend\ServiceManager\Config object or array to
* seed such an object.
*
* #return array|\Zend\ServiceManager\Config
*/
public function getServiceConfig()
{
return [
'factories' => [
Model\PostTable::class => function($container) {
$tableGateway = $container->get(Model\PostTableGateway::class);
return new Model\PostTable($tableGateway);
},
Model\PostTableGateway::class => function ($container) {
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\Post());
return new TableGateway('post', $dbAdapter, null, $resultSetPrototype);
},
],
];
}
public function getControllerConfig() {
return [
'factories' => [
Controller\PostController::class => function($container) {
return new Controller\PostController(
$container->get(Model\PostTable::class)
);
},
],
];
}
}
Most probably you have a duplicate setting for the controller factory in the module.config.php. Check:
'controllers' => [
'factories' => [
Controller\PostController::class => InvokableFactory::class,
...
],
],
Remove line:
Controller\PostController::class => InvokableFactory::class,
so that the factory method in your Module.php is used.
In PHP i have function like this in which it has a call to another function searchDateOperator() with in the same class using $this keyword. How to write a Unit test for this?
public static function segmetDateRangeFilter($searchField, $startDate, $endDate, $dateRange)
{
$filter = [];
if ($startDate && !$endDate) {
$filter = [
'range' => [
$this->searchDateOperator($searchField) => [
'gte' => strtotime($startDate),
]
]
];
}
if ($endDate && !$startDate) {
$filter = [
'range' => [
$this->searchDateOperator($searchField) => [
'lte' => strtotime($endDate)
]
]
];
}
if ($startDate && $endDate) {
$filter = [
'range' => [
$this->searchDateOperator($searchField) => [
'gte' => strtotime($startDate),
'lte' => strtotime($endDate)
]
]
];
}
if ($dateRange !== '') {
// $endTime upto current Time
$endTime = Carbon::now()->timestamp;
// Start Time . substract the date range days. and in timestamp
$startTime = Carbon::now()->subDays($dateRange)->timestamp;
$filter = [
'range' => [
$this->searchDateOperator($searchField) => [
'gte' => $startTime,
'lte' => $endTime
]
]
];
}
if ($filter) {
return $filter;
}
}
Your problem is that the method is declared static but you are using $this. If the method you are calling is static as well you should use self or static, like this:
$filter = [
'range' => [
self::searchDateOperator($searchField) => [
'gte' => $startTime,
'lte' => $endTime
]
]
];
Alternatively you could remove static from segmetDateRangeFilter and use it in a test like this:
public function testSomething()
{
$filterFactory = new DateFilterFactory();
// ...
$result = $filterFactory->segmetDateRangeFilter(...);
// Assertions against result
}
The class name DateFilterFactory you have to replace with whatever you use.