Validation in Zend Framework 2 with Doctrine 2 - php

I am right now getting myself more and more familiar with Zend Framework 2 and in the meantime I was getting myself updated with the validation part in Zend Framework 2. I have seen few examples how to validate the data from the database using Zend Db adapter, for example the code from the Zend Framework 2 official website:
//Check that the username is not present in the database
$validator = new Zend\Validator\Db\NoRecordExists(
array(
'table' => 'users',
'field' => 'username'
)
);
if ($validator->isValid($username)) {
// username appears to be valid
} else {
// username is invalid; print the reason
$messages = $validator->getMessages();
foreach ($messages as $message) {
echo "$message\n";
}
}
Now my question is how can do the validation part?
For example, I need to validate a name before inserting into database to check that the same name does not exist in the database, I have updated Zend Framework 2 example Album module to use Doctrine 2 to communicate with the database and right now I want to add the validation part to my code.
Let us say that before adding the album name to the database I want to validate that the same album name does not exist in the database.
Any information regarding this would be really helpful!

if you use the DoctrineModule, there is already a validator for your case.

I had the same problem and solved it this way:
Create a custom validator class, name it something like NoEntityExists (or whatever you want).
Extend Zend\Validator\AbstractValidator
Provide a getter and setter for Doctrine\ORM\EntityManager
Provide some extra getters and setters for options (entityname, ...)
Create an isValid($value) method that checks if a record exists and returns a boolean
To use it, create a new instance of it, assign the EntityManager and use it just like any other validator.
To get an idea of how to implement the validator class, check the validators that already exist (preferably a simple one like Callback or GreaterThan).
Hope I could help you.
// Edit: Sorry, I'm late ;-)
So here is a quite advanced example of how you can implement such a validator.
Note that I added a translate() method in order to catch language strings with PoEdit (a translation helper tool that fetches such strings from the source codes and puts them into a list for you). If you're not using gettext(), you can problably skip that.
Also, this was one of my first classes with ZF2, I wouldn't put this into the Application module again. Maybe, create a new module that fits better, for instance MyDoctrineValidator or so.
This validator gives you a lot of flexibility as you have to set the query before using it. Of course, you can pre-define a query and set the entity, search column etc. in the options. Have fun!
<?php
namespace Application\Validator\Doctrine;
use Zend\Validator\AbstractValidator;
use Doctrine\ORM\EntityManager;
class NoEntityExists extends AbstractValidator
{
const ENTITY_FOUND = 'entityFound';
protected $messageTemplates = array();
/**
* #var EntityManager
*/
protected $entityManager;
/**
* #param string
*/
protected $query;
/**
* Determines if empty values (null, empty string) will <b>NOT</b> be included in the check.
* Defaults to true
* #var bool
*/
protected $ignoreEmpty = true;
/**
* Dummy to catch messages with PoEdit...
* #param string $msg
* #return string
*/
public function translate($msg)
{
return $msg;
}
/**
* #return the $ignoreEmpty
*/
public function getIgnoreEmpty()
{
return $this->ignoreEmpty;
}
/**
* #param boolean $ignoreEmpty
*/
public function setIgnoreEmpty($ignoreEmpty)
{
$this->ignoreEmpty = $ignoreEmpty;
return $this;
}
/**
*
* #param unknown_type $entityManager
* #param unknown_type $query
*/
public function __construct($entityManager = null, $query = null, $options = null)
{
if(null !== $entityManager)
$this->setEntityManager($entityManager);
if(null !== $query)
$this->setQuery($query);
// Init messages
$this->messageTemplates[self::ENTITY_FOUND] = $this->translate('There is already an entity with this value.');
return parent::__construct($options);
}
/**
*
* #param EntityManager $entityManager
* #return \Application\Validator\Doctrine\NoEntityExists
*/
public function setEntityManager(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
return $this;
}
/**
* #return the $query
*/
public function getQuery()
{
return $this->query;
}
/**
* #param field_type $query
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* #return \Doctrine\ORM\EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
/**
* (non-PHPdoc)
* #see \Zend\Validator\ValidatorInterface::isValid()
* #throws Exception\RuntimeException() in case EntityManager or query is missing
*/
public function isValid($value)
{
// Fetch entityManager
$em = $this->getEntityManager();
if(null === $em)
throw new Exception\RuntimeException(__METHOD__ . ' There is no entityManager set.');
// Fetch query
$query = $this->getQuery();
if(null === $query)
throw new Exception\RuntimeException(__METHOD__ . ' There is no query set.');
// Ignore empty values?
if((null === $value || '' === $value) && $this->getIgnoreEmpty())
return true;
$queryObj = $em->createQuery($query)->setMaxResults(1);
$entitiesFound = !! count($queryObj->execute(array(':value' => $value)));
// Set Error message
if($entitiesFound)
$this->error(self::ENTITY_FOUND);
// Valid if no records are found -> result count is 0
return ! $entitiesFound;
}
}

Related

static and non static methods in mvc websites

currently i have a problem which don't allow me to continue adding features to my mvc website without do any sort of spaghetti code.
i have two classes, one is ModModel and the other is ModUploadModel. both are extended with the Model class.
ModModel contains all the methods about "mods", as ModModel->doesModNameExists(), ModModel->getModDetails() etc...
ModUploadModel contains all the methods for the uploading of a mod, as ModUploadModel->upload(), ModUploadModel->isModNameValid() etc...
in some cases i have to call some ModModel methods from ModUploadModel, and to do so i have to create a new instance of ModModel inside the ModUploadController and to pass it as an argument to ModUploadModel->upload().
for example: the ModUploadController creates two new objects, $modModel = new ModModel() and $modUploadModel = new ModUploadModel(), then calls $modUploadModel->upload($modModel).
this is the ModUploadController, which creates the two objects and call the ModUploadModel->upload() method
class ModUploadController extends Mvc\Controller {
public function uploadMod(): void {
$modUploadModel = new ModUploadModel()
$modModel = new ModModel();
// $modModel needs to be passed because the ModUploadModel needs
// one of its methods
if ($modUploadModel->upload("beatiful-mod", $modModel)) {
// success
} else {
// failure
}
}
}
ModUploadModel->upload() checks if the input is valid (if the mod name isn't already taken etc), and finally upload the mod data into the db. obviously it's all suddivise in more sub private methods, as ModUploadModel->isModNameValid() and ModUploadModel->insertIntoDb().
the problem is that i don't structured my classes with all static methods, and everytime i have to pass objects as parameters, like with ModModel (for example i need its isModNameValid() method).
i thought about making all the ModModel methods static, but that's not as simple as it seems, because all its methods query the db, and they use the Model->executeStmt() method (remember that all the FooBarModel classes are extended with the Model class, which contains usefull common methods as executeStmt() and others), and calling a non static method from a static one is not a good practice in php, so i should make static the Model methods too, and consequently also the Dbh methods for the db connection (Model is extended with Dbh).
the ModModel class:
class ModModel extends Mvc\Model {
// in reality it queries the db with $this->executeStmt(),
// which is a Model method
public function doesModNameExists($name) {
if (/* exists */) {
return true;
}
return false;
}
}
the ModUploadModel class:
class ModUploadModel extends Mvc\Model {
private $modName;
public function upload($modName, $modModel) {
$this->modName = $modName;
if (!$this->isModNameValid($modModel)) {
return false;
}
if ($this->insertIntoDb()) {
return true;
}
return false;
}
// this methods needs to use the non static doesModNameExists() method
// which is owned by the ModModel class, so i need to pass
// the object as an argument
private function isModNameValid($modModel) {
if ($modModel->doesModNameExists($this->modName)) {
return false;
}
// other if statements
return true;
}
private function insertIntoDb() {
$sql = "INSERT INTO blabla (x, y) VALUES (?, ?)";
$params = [$this->modName, "xxx"];
if ($this->executeStmt($sql, $params)) {
return true;
}
return false;
}
}
the alternative would be to create a new instance of Model inside the ModModel methods, for example (new Model)->executeStmt(). the problem is that it's not a model job to create new objects and generally it's not the solution i like most.
Some observations and suggestions:
[a] You are passing a ModModel object to ModUploadModel to validate the mod name before uploading. You shouldn't even try to call ModUploadModel::upload() if a mod with the provided name already exists. So you should follow steps similar to this:
class ModUploadController extends Mvc\Controller {
public function uploadMod(): void {
$modUploadModel = new ModUploadModel()
$modModel = new ModModel();
$modName = 'beatiful-mod';
try {
if ($modModel->doesModNameExists($modName)) {
throw new \ModNameExistsException('A mod with the name "' . $modName . '" already exists');
}
$modUploadModel->upload($modName);
} catch (\ModNameExistsException $exception){
// ...Present the exception message to the user. Use $exception->getMessage() to get it...
}
}
}
[b] Creating objects inside a class is a bad idea (like in ModUploadController). Use dependency injection instead. Read this and watch this and this. So the solution would look something like this:
class ModUploadController extends Mvc\Controller {
public function uploadMod(ModUploadModel $modUploadModel, ModModel $modModel): void {
//... Use the injected objects ($modUploadModel and $modModel ) ...
}
}
In a project, all objects that need to be injected into others can be created by a "dependency injection container". For example, PHP-DI (which I recommend), or other DI containers. So, a DI container takes care of all dependency injections of your project. For example, in your case, the two objects injected into ModUploadController::uploadMod method would be automatically created by PHP-DI. You'd just have to write three lines of codes in the file used as the entry-point of your app, probably index.php:
use DI\ContainerBuilder;
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$container = $containerBuilder->build();
Of course, a DI container requires configuration steps as well. But, in a couple of hours, you can understand how and where to do it.
By using a DI container, you'll be able to concentrate yourself solely on the logic of your project, not on how and where various components should be created, or similar tasks.
[c] Using static methods is a bad idea. My advise would be to get rid of all static methods that you already wrote. Watch this, read this, this and this. So the solution to the injection problem(s) that you have is the one above: the DI, perfomed by a DI container. Not at all creating static methods.
[d] You are using both components to query the database (ModModel with doesModNameExists() and ModUploadModel with insertIntoDb()). You should dedicate only one component to deal with the database.
[e] You don't need Mvc\Model at all.
[f] You don't need Mvc\Controller at all.
Some code:
I wrote some code, as an alternative to yours (from which I somehow "deduced" the tasks). Maybe it will help you, seeing how someone else would code. It would give you the possibility of "adding features to my mvc website without do any sort of spaghetti code". The code is very similar to the one from an answer that I wrote a short time ago. That answer also contains additional important suggestions and resources.
Important: Note that the application services, e.g. all components from Mvc/App/Service/, should communicate ONLY with the domain model components, e.g. with the components from Mvc/Domain/Model/ (mostly interfaces), not from Mvc/Domain/Infrastructure/. In turn, the DI container of your choice will take care of injecting the proper class implementations from Mvc/Domain/Infrastructure/ for the interfaces of Mvc/Domain/Model/ used by the application services.
Note: my code uses PHP 8.0. Good luck.
Project structure:
Mvc/App/Controller/Mod/AddMod.php:
<?php
namespace Mvc\App\Controller\Mod;
use Psr\Http\Message\{
ResponseInterface,
ServerRequestInterface,
};
use Mvc\App\Service\Mod\{
AddMod As AddModService,
Exception\ModAlreadyExists,
};
use Mvc\App\View\Mod\AddMod as AddModView;
class AddMod {
/**
* #param AddModView $addModView A view for presenting the response to the request back to the user.
* #param AddModService $addModService An application service for adding a mod to the model layer.
*/
public function __construct(
private AddModView $addModView,
private AddModService $addModService,
) {
}
/**
* Add a mod.
*
* The mod details are submitted from a form, using the HTTP method "POST".
*
* #param ServerRequestInterface $request A server request.
* #return ResponseInterface The response to the current request.
*/
public function addMod(ServerRequestInterface $request): ResponseInterface {
// Read the values submitted by the user.
$name = $request->getParsedBody()['name'];
$description = $request->getParsedBody()['description'];
// Add the mod.
try {
$mod = $this->addModService->addMod($name, $description);
$this->addModView->setMod($mod);
} catch (ModAlreadyExists $exception) {
$this->addModView->setErrorMessage(
$exception->getMessage()
);
}
// Present the results to the user.
$response = $this->addModView->addMod();
return $response;
}
}
Mvc/App/Service/Mod/Exception/ModAlreadyExists.php:
<?php
namespace Mvc\App\Service\Mod\Exception;
/**
* An exception thrown if a mod already exists.
*/
class ModAlreadyExists extends \OverflowException {
}
Mvc/App/Service/Mod/AddMod.php:
<?php
namespace Mvc\App\Service\Mod;
use Mvc\Domain\Model\Mod\{
Mod,
ModMapper,
};
use Mvc\App\Service\Mod\Exception\ModAlreadyExists;
/**
* An application service for adding a mod.
*/
class AddMod {
/**
* #param ModMapper $modMapper A data mapper for transfering mods
* to and from a persistence system.
*/
public function __construct(
private ModMapper $modMapper
) {
}
/**
* Add a mod.
*
* #param string|null $name A mod name.
* #param string|null $description A mod description.
* #return Mod The added mod.
*/
public function addMod(?string $name, ?string $description): Mod {
$mod = $this->createMod($name, $description);
return $this->storeMod($mod);
}
/**
* Create a mod.
*
* #param string|null $name A mod name.
* #param string|null $description A mod description.
* #return Mod The newly created mod.
*/
private function createMod(?string $name, ?string $description): Mod {
return new Mod($name, $description);
}
/**
* Store a mod.
*
* #param Mod $mod A mod.
* #return Mod The stored mod.
* #throws ModAlreadyExists The mod already exists.
*/
private function storeMod(Mod $mod): Mod {
if ($this->modMapper->modExists($mod)) {
throw new ModAlreadyExists(
'A mod with the name "' . $mod->getName() . '" already exists'
);
}
return $this->modMapper->saveMod($mod);
}
}
Mvc/App/View/Mod/AddMod.php:
<?php
namespace Mvc\App\View\Mod;
use Mvc\{
App\View\View,
Domain\Model\Mod\Mod,
};
use Psr\Http\Message\ResponseInterface;
/**
* A view for adding a mod.
*/
class AddMod extends View {
/** #var Mod A mod. */
private Mod $mod = null;
/**
* Add a mod.
*
* #return ResponseInterface The response to the current request.
*/
public function addMod(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Mod/AddMod.html.twig', [
'activeNavItem' => 'AddMod',
'mod' => $this->mod,
'error' => $this->errorMessage,
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Set the mod.
*
* #param Mod $mod A mod.
* #return static
*/
public function setMod(Mod $mod): static {
$this->mod = $mod;
return $this;
}
}
Mvc/App/View/View.php:
<?php
namespace Mvc\App\View;
use Psr\Http\Message\ResponseFactoryInterface;
use SampleLib\Template\Renderer\TemplateRendererInterface;
/**
* A view.
*/
abstract class View {
/** #var string An error message */
protected string $errorMessage = '';
/**
* #param ResponseFactoryInterface $responseFactory A response factory.
* #param TemplateRendererInterface $templateRenderer A template renderer.
*/
public function __construct(
protected ResponseFactoryInterface $responseFactory,
protected TemplateRendererInterface $templateRenderer
) {
}
/**
* Set the error message.
*
* #param string $errorMessage An error message.
* #return static
*/
public function setErrorMessage(string $errorMessage): static {
$this->errorMessage = $errorMessage;
return $this;
}
}
Mvc/Domain/Infrastructure/Mod/PdoModMapper.php:
<?php
namespace Mvc\Domain\Infrastructure\Mod;
use Mvc\Domain\Model\Mod\{
Mod,
ModMapper,
};
use PDO;
/**
* A data mapper for transfering Mod entities to and from a database.
*
* This class uses a PDO instance as database connection.
*/
class PdoModMapper implements ModMapper {
/**
* #param PDO $connection Database connection.
*/
public function __construct(
private PDO $connection
) {
}
/**
* #inheritDoc
*/
public function modExists(Mod $mod): bool {
$sql = 'SELECT COUNT(*) as cnt FROM mods WHERE name = :name';
$statement = $this->connection->prepare($sql);
$statement->execute([
':name' => $mod->getName(),
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data['cnt'] > 0) ? true : false;
}
/**
* #inheritDoc
*/
public function saveMod(Mod $mod): Mod {
if (isset($mod->getId())) {
return $this->updateMod($mod);
}
return $this->insertMod($mod);
}
/**
* Update a mod.
*
* #param Mod $mod A mod.
* #return Mod The mod.
*/
private function updateMod(Mod $mod): Mod {
$sql = 'UPDATE mods
SET
name = :name,
description = :description
WHERE
id = :id';
$statement = $this->connection->prepare($sql);
$statement->execute([
':name' => $mod->getName(),
':description' => $mod->getDescription(),
]);
return $mod;
}
/**
* Insert a mod.
*
* #param Mod $mod A mod.
* #return Mod The newly inserted mod.
*/
private function insertMod(Mod $mod): Mod {
$sql = 'INSERT INTO mods (
name,
description
) VALUES (
:name,
:description
)';
$statement = $this->connection->prepare($sql);
$statement->execute([
':name' => $mod->getName(),
':description' => $mod->getDescription(),
]);
$mod->setId(
$this->connection->lastInsertId()
);
return $mod;
}
}
Mvc/Domain/Model/Mod/Mod.php:
<?php
namespace Mvc\Domain\Model\Mod;
/**
* Mod entity.
*/
class Mod {
/**
* #param string|null $name (optional) A name.
* #param string|null $description (optional) A description.
*/
public function __construct(
private ?string $name = null,
private ?string $description = null
) {
}
/**
* Get id.
*
* #return int|null
*/
public function getId(): ?int {
return $this->id;
}
/**
* Set id.
*
* #param int|null $id An id.
* #return static
*/
public function setId(?int $id): static {
$this->id = $id;
return $this;
}
/**
* Get the name.
*
* #return string|null
*/
public function getName(): ?string {
return $this->name;
}
/**
* Set the name.
*
* #param string|null $name A name.
* #return static
*/
public function setName(?string $name): static {
$this->name = $name;
return $this;
}
/**
* Get the description.
*
* #return string|null
*/
public function getDescription(): ?string {
return $this->description;
}
/**
* Set the description.
*
* #param string|null $description A description.
* #return static
*/
public function setDescription(?string $description): static {
$this->description = $description;
return $this;
}
}
Mvc/Domain/Model/Mod/ModMapper.php:
<?php
namespace Mvc\Domain\Model\Mod;
use Mvc\Domain\Model\Mod\Mod;
/**
* An interface for various data mappers used to
* transfer Mod entities to and from a persistence system.
*/
interface ModMapper {
/**
* Check if a mod exists.
*
* #param Mod $mod A mod.
* #return bool True if the mod exists, false otherwise.
*/
public function modExists(Mod $mod): bool;
/**
* Save a mod.
*
* #param Mod $mod A mod.
* #return Mod The saved mod.
*/
public function saveMod(Mod $mod): Mod;
}

Array Field in MongoDB Object is returned empty by Doctrine although it is not empty in DB

I'm working on a Symfony Application which uses a MongoDB and Doctrine to Connect the Application with the DB.
But now I added a collection field to one of the documents but the content of this field is always empty when I query it from the database via PHP, although it is not empty, when I return it directly in the MongoDB Shell.
In more Detail:
I added a new field 'loginLast90Days' to the existing document 'User' to store the time integers in an array:
<?php
namespace AppBundle\Document;
use Doctrine\Bundle\MongoDBBundle\Validator\Constraints\Unique as MongoDBUnique;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #MongoDB\Document(collection="users")
* #MongoDBUnique(fields="{id, email}")
*/
class User
{
// ...
/**
* #MongoDB\Field(type="collection")
*/
protected $loginLast90Days;
// ...
/**
* Set loginLast90Days
*
* #param collection $loginLast90Days
* #return $this
*/
public function setLoginLast90Days($loginLast90Days)
{
$this->loginLast90Days = $loginLast90Days;
return $this;
}
/**
* Get loginLast90Days
*
* #return collection $loginLast90Days
*/
public function getLoginLast90Days()
{
return $this->loginLast90Days;
}
public function addLoginLast90Days($login)
{
$this->loginLast90Days[] = $login;
return $this;
}
public function removeLoginLast90Days($login)
{
$this->loginLast90Days->removeElement($login);
return $this;
}
// ...
}
Now I can add a date integer to one of the user objects:
$dm = $this->get('doctrine_mongodb');
$user = $dm->getRepository('AppBundle:User')->findOneBy(array('gmail' => $email));
$user->addLoginLast90Days(time());
$dm = $this->get('doctrine_mongodb')->getManager();
$dm->persist($user);
$dm->flush();
Now when I find the object using the Mongo Shell I can see the new value in the field.
But the next time I add a value to the object, the array in PHP is empty, although I can see in the DB that it should not be empty.
Does anyone have an idea, where my mistake is? Am I forgetting to map the field to the db correctly or something?
Thanks so much in advance!
Edit:
Here is the constructor of the User document:
public function __construct($customerId, $refreshToken, $canManageCustomers)
{
$this->customerId = $customerId;
$this->refreshToken = $refreshToken;
$this->canManageCustomers = $canManageCustomers;
$this->isActive = true;
$this->accountLevel = "basic";
$this->isRegistered = false;
$this->createdAt = new \MongoDate();
}
Try to initialize your loginLast90Days as an Array,
initialize the attribute loginLast90Days in your constructor
public function __construct()
{
//Whatever you had here...
$this->loginLast90Days = [];
}

PHPUnit data provider dynamically creation

I have very interesting question about PHPUnit data providers.
protected $controller;
protected function setUp()
{
$this->controller = new ProductController();
}
/**
* #covers ProductsController::createItem
* #dataProvider getTestDataProvider
* #param number $name
*/
public function testCreateItem($name)
{
$prod = $this->controller->createItem($name);
$id = $prod->getId;
$this->assertInternalType('int', $id);
$this->assertInstanceOf('Product', $prod);
}
/**
* #covers ProductsController::getItemInfo
* #depends testCreateItem
* #param number $id
*/
public function testGetItemInfo($id)
{
$info = $this->controller->getItemInfo($id);
$this->assertArrayHasKey('id',$info);
$this->assertEquals($id, $info['id']);
}
I use getTestDataProvider to get test data from CSV file. Then testCreateItem create 10 new products from CSV rows.
How can I create an array of $id of new products and use it as Data provider for testGetItemInfo? I can't store it in SESSION or file because provider functions run's before SetUp.
Maybe someone has already faced a similar problem?
I have only idea with static field (maybe not the best, but if someone has better I'll look).
private static $ids;
/**
* #dataProvider some
*/
public function testT1($id)
{
self::$ids[] = $id;
}
/**
* #depends testT1
*/
public function testT2()
{
var_dump(self::$ids);
}
public function some()
{
return [
[1],
[2],
[3]
];
}
You must remember that field is visible in all class so if you want use another data set you must nullify this field.

FOSRestBundle: partial response in function of attributes asked in the request

Context
I found a lot of questions about partial API response with FOSRest and all the answers are based on the JMS serializer options (exlude, include, groups, etc). It works fine but I try to achieve something less "static".
Let's say I have a user with the following attributes: id username firstname lastname age sex
I retrieve this user with the endpoint GET /users/{id} and the following method:
/**
* #View
*
* GET /users/{id}
* #param integer $user (uses ParamConverter)
*/
public function getUserAction(User $user) {
return $user;
}
The method returns the user with all his attributes.
Now I want to allow something like that: GET /users/{id}?attributes=id,username,sex
Question
Did I missed a functionality of FOSRestBUndle, JMSserializer or SensioFrameworkExtraBundle to achieve it automatically? An annotation, a method, a keyword in the request or something else?
Otherwise, what is the best way to achieve it?
Code
I thought to do something like that:
/**
* #View
* #QueryParam(name="attributes")
*
* GET /users/{id}
*
* #param integer $user (uses ParamConverter)
*/
public function getUserAction(User $user, $attributes) {
$groups = $attributes ? explode(",", $attributes) : array("Default");
$view = $this->view($user, 200)
->setSerializationContext(SerializationContext::create()->setGroups($groups));
return $this->handleView($view);
}
And create a group for each attribute:
use JMS\Serializer\Annotation\Groups;
class User {
/** #Groups({"id"}) */
protected $id;
/** #Groups({"username"}) */
protected $username;
/** #Groups({"firstname"}) */
protected $firstname;
//etc
}
My implementation based on Igor's answer:
ExlusionStrategy:
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsExclusionStrategy implements ExclusionStrategyInterface {
private $fields = array();
public function __construct(array $fields) {
$this->fields = $fields;
}
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext) {
return false;
}
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext) {
if (empty($this->fields)) {
return false;
}
if (in_array($property->name, $this->fields)) {
return false;
}
return true;
}
}
Controller:
/**
* #View
* #QueryParam(name="fields")
*
* GET /users/{id}
*
* #param integer $user (uses ParamConverter)
*/
public function getUserAction(User $user, $fields) {
$context = new SerializationContext();
$context->addExclusionStrategy(new FieldsExclusionStrategy($fields ? explode(',', $fields) : array()));
return $this->handleView($this->view($user)->setSerializationContext($context));
}
Endpoint:
GET /users/{id}?fields=id,username,sex
You can do it like that through groups, as you've shown. Maybe a bit more elegant solution would be to implement your own ExclusionStrategy. #Groups and other are implementations of ExclusionStrategyInterface too.
So, say you called your strategy SelectFieldsStrategy. Once you implement it, you can add it to your serialization context very easy:
$context = new SerializationContext();
$context->addExclusionStrategy(new SelectFieldsStrategy(['id', 'name', 'someotherfield']));

Singleton instance, how to write write self

I am looking for a way to extend our default logging class without making changes to the whole application or library. We have a number of places where we write logs. E.g:
App_Log::getInstance()->write(
$name,
$type,
"LOGOUT",
$url
);
Auth_Log
<?php
class App_Auth_Log {
/**
* Singleton instance
*
* Marked only as protected to allow extension of the class. To extend,
* simply override {#link getInstance()}.
*
* #var App_Auth_Log
*/
protected static $_instance = null;
/**
* Auth logging enabled flag.
*
* #var boolean
*/
protected $_enabled = false;
/**
* If flag is true then cleanup will not remove login records.
*
* #var boolean
*/
protected $_keepLoginRecords = false;
/**
* Class constructor.
*/
public function __construct() {
if(App_Front::getInstance()->hasParam("withAuthLog"))
$this->_enabled = true;
if(App_Front::getInstance()->hasParam("withKeepLoginRecords"))
$this->_keepLoginRecords = true;
$this->cleanup();
}
/**
* Singleton instance
*
* #return App_Auth_Log
*/
public static function getInstance() {
if (is_null(self::$_instance))
self::$_instance = new self();
return self::$_instance;
}
/**
* Write new auth log record with given details. if succesful then method
* returns true otherwise returns false.
*
* #param string $class
* #param string $ident
* #param string $action
* #param string $url
* #param string $ipaddr
* #return boolean
* #throws Exception
*/
public function write($class,$ident,$action,$url,$ipaddr=null) {
if($this->isEnabled()) {
$db = App_Db_Connections::getInstance()->getConnection();
try {
// if address not specificed get remote addr
$ipaddr = ($ipaddr == null) ? $_SERVER['REMOTE_ADDR'] : $ipaddr;
// manual insert so we can take advantage of insert delayed
$stmnt = "INSERT INTO accesslogs
VALUES('',NOW(),'$class','$ident','$action','$url','$ipaddr')";
// execute insert
$db->query($stmnt);
} catch (Exception $e) {
throw $e;
}
return true;
}
return false;
}
/**
* Cleanup old accesslog records. Cached to run once a day.
*
* #return boolean - returns true if run false if not.
*/
public function cleanup() {
$cache = App_Cache::getInstance()->newObject(86400);
if($this->isEnabled()) {
if (!$res = $cache->load(App_Cache::getCacheName(__CLASS__. "cleanup"))) {
// add cache
$db = App_Db_Connections::getInstance()->getConnection();
try {
$where = $db->quoteInto("DATEDIFF(NOW(),accesslog_datetime) > ?", 6);
$and = ($this->_keepLoginRecords) ? " AND accesslog_action != 'LOGIN'" : "";
$db->query("DELETE LOW_PRIORITY FROM accesslogs WHERE $where $and");
} catch (Exception $e) {
throw $e;
}
$cache->save($res,App_Cache::getCacheName(__CLASS__. "cleanup"));
} // end cache
}
return;
}
/**
* Returns boolean check if auth log enabled.
*
* #return boolean
*/
public function isEnabled() {
return ($this->_enabled) ? true : false;
}
/**
* Enabled disable the auth log process.
*
* #param boolean $boolean
* #return App_Auth_Log
*/
public function setEnabled($boolean) {
$this->_enabled = ($boolean) ? true : false;
return $this;
}
}
?>
This is the default behaviour of the core code. But for this specific project I need to be able to extend/overwrite the write method, e.g with extra parameters.
Q: How can I make changes to this App_Auth_Log class so that its backwards compatible with previous projects that call App_Log::getInstance()->write?
How I think it should work(but dont know how to do it).
If App_Front::getInstance()->hasParam("withAuthLog") passes a custom class name e.g: My_Custom_Auth_Log which overwrites the original write method. Just not sure how to modify the singleton part
You have no choice. You have to modify your App_Log code, because everything statically makes calls to it. For minimal changes, you could extend the App_Log class and make App_Log::getInstance return an instance of that child class; but that's pretty messy, since a parent should never know about its children.
Maybe you can prevent the default implementation of App_Log to be loaded and load a different implementation of it from a different file. That's pretty messy too though.
This is exactly (one of) the reason(s) why singletons and static calls are very frowned upon. See How Not To Kill Your Testability Using Statics.

Categories