Decode multiple model in php symfony from json to object - php

I would like to convert a json into a object / model.
If the json is only one-dimensional, it works perfectly.
But if it is multidimensional, only the outer (user) is converted, but not the inner (company), this remains an array.
Can you help me with this?
The Models:
<?php
namespace AppBundle;
class Company {
/**
* #var string
*/
protected $companyName = '';
/**
* #return string
*/
public function getCompanyName()
{
return $this->companyName;
}
/**
* #param string $companyName
* #return void
*/
public function setCompanyName($companyName)
{
$this->companyName = $companyName;
}
}
class User {
/**
* #var \AppBundle\Company
*/
protected $company = null;
/**
* #var string
*/
protected $username = '';
/**
* #return \AppBundle\Company
*/
public function getCompany() {
return $this->company;
}
/**
* #param \AppBundle\Company $company
* #return void
*/
public function setCompany($company) {
$this->company = $company;
}
/**
* #return string
*/
public function getUsername() {
return $this->username;
}
/**
* #param string $username
* #return void
*/
public function setUsername($username) {
$this->username = $username;
}
}
?>
Convert json to model:
<?php
namespace AppBundle\Controller;
class DefaultController extends \Symfony\Bundle\FrameworkBundle\Controller\Controller
{
public function indexAction()
{
// Initialize serializer
$objectNormalizer = new \Symfony\Component\Serializer\Normalizer\ObjectNormalizer();
$jsonEncoder = new \Symfony\Component\Serializer\Encoder\JsonEncoder();
$serializer = new \Symfony\Component\Serializer\Serializer([$objectNormalizer], [$jsonEncoder]);
// Set test model
$company = new \AppBundle\Company();
$company->setCompanyName('MyCompany');
$user = new \AppBundle\User();
$user->setCompany($company);
$user->setUsername('MyUsername');
// Serialize test model to json
$json = $serializer->serialize($user, 'json');
dump($user); // Model ok, Company is instance of \AppBundle\Company
dump($json); // json ok + valide
// Deserialize json to model
$user = $serializer->deserialize($json, \AppBundle\User::class, 'json');
dump($user); // Error: Company is now array instead instance of \AppBundle\Company
// Denormalize json to model
$userArray = $serializer->decode($json, 'json');
$user = $serializer->denormalize($userArray, \AppBundle\User::class);
dump($user); // Error: Company is now array instead instance of \AppBundle\Company
}
}
?>

I solved the problem.
On the one hand you need PHP 7. Annotations I have not yet tested.
Then the variable has to be set correctly in setCompany().
public function setCompany(Company $company) {
$this->company = $company;
}
And the ReflectionExtractor() must be used.
use Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyInfo\Extractor;
$objectNormalizer = new ObjectNormalizer(
null,
null,
null,
new ReflectionExtractor()
);
You only need deserialize(), because it decode() and denormalize().
http://symfony.com/doc/current/components/serializer.html
Full fixed code:
Company class:
class Company {
/**
* #var string
*/
protected $companyName = '';
/**
* #return string
*/
public function getCompanyName() {
return $this->companyName;
}
/**
* #param string $companyName
* #return void
*/
public function setCompanyName($companyName) {
$this->companyName = $companyName;
}
}
User class:
class User {
/**
* #var \AppBundle\Company
*/
protected $company = null;
/**
* #var string
*/
protected $username = '';
/**
* #return \AppBundle\Company
*/
public function getCompany() {
return $this->company;
}
/**
* #param \AppBundle\Company $company
* #return void
*/
public function setCompany(Company $company) {
$this->company = $company;
}
/**
* #return string
*/
public function getUsername() {
return $this->username;
}
/**
* #param string $username
* #return void
*/
public function setUsername($username) {
$this->username = $username;
}
}
?>
Controller class:
<?php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller;
use Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyInfo\Extractor;
use Symfony\Component\Serializer;
class DefaultController extends Controller {
public function indexAction() {
$objectNormalizer = new ObjectNormalizer(
null,
null,
null,
new ReflectionExtractor()
);
$jsonEncoder = new JsonEncoder();
$serializer = new Serializer([$objectNormalizer], [$jsonEncoder]);
$company = new \AppBundle\Company();
$company->setCompanyName('MyCompany');
$user = new \AppBundle\User();
$user->setCompany($company);
$user->setUsername('MyUsername');
$json = $serializer->serialize($user, 'json');
dump($user, $json);
$user2 = $serializer->deserialize($json, \AppBundle\User::class, 'json');
dump($user2);
}
}
?>

Related

With doctrine ODM, can I embed many subdocuments in a main document?

I try to save this JSON:
{
"vendorId": "vendor-fc162cdffd73",
"company": {
"companyId": "bcos1.company.1806cf97-a756-4fbf-9081-fc162cdffd73",
"companyVersion": 1,
"companyName": "Delivery Inc.",
"address": {
"streetAddress": "300 Boren Ave",
"city": "Seattle",
"region": "US-WA",
"country": "US",
"postalCode": "98109",
"storeName": "Seattle Store",
"coordinates": {
"latitude": "45.992820",
"longitude": "45.992820"
}
},
"emailAddress": "johndoe#amazon.com",
"phoneNumber": "1234567890",
"websiteUrl": "delivery.com",
"creationDate": "2022-03-06T21:00:52.222Z"
},
"creationDate": "2022-04-06T21:00:52.222Z"
}
Company is a subdocument this has address and address has coordinates subdocument.
When I try to save with Hydratation, see example:
https://www.doctrine-project.org/projects/doctrine-laminas-hydrator/en/3.0/basic-usage.html#example-4-embedded-entities
I got this error:
1) AppTest\Services\AccountsServiceTest::testNewAccount with data set #0 (array('{"companyId":"bcos1.com...222Z"}', '', ''))
array_flip(): Can only flip STRING and INTEGER values!
vendor/doctrine/doctrine-laminas-hydrator/src/DoctrineObject.php:488
vendor/doctrine/doctrine-laminas-hydrator/src/DoctrineObject.php:355
vendor/doctrine/doctrine-laminas-hydrator/src/DoctrineObject.php:165
src/App/Document/Repository/AccountRepository.php:67
In DoctrineObject line 488
protected function toOne(string $target, $value): ?object
{
$metadata = $this->objectManager->getClassMetadata($target);
if (is_array($value) && array_keys($value) !== $metadata->getIdentifier()) {
// $value is most likely an array of fieldset data
$identifiers = array_intersect_key(
$value,
array_flip($metadata->getIdentifier())
);
$object = $this->find($identifiers, $target) ?: new $target();
return $this->hydrate($value, $object);
}
return $this->find($value, $target);
}
My code:
$vendorAccountId = uniqid('vendor-account-id-');
$account = new Account();
$hydrator->hydrate($data, $account);
My main Entity:
<?php
namespace App\Document\Entity;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/**
* #MongoDB\Document(db="awesome-company", collection="Account", repositoryClass="App\Document\Repository\AccountRepository")
*/
class Account
{
/** #MongoDB\Id(name="_id") */
private string $id;
/** #MongoDB\Field(type="string", name="vendorAccountId") */
private string $vendorAccountId;
/**
* #return string
*/
public function getVendorAccountId(): string
{
return $this->vendorAccountId;
}
/**
* #param string $vendorAccountId
*/
public function setVendorAccountId(string $vendorAccountId): void
{
$this->vendorAccountId = $vendorAccountId;
}
/**
* #MongoDB\EmbedOne(targetDocument=Company::class)
*/
private Company $company;
/**
* #MongoDB\Field(type="string",name="realm")
**/
private $realm;
/**
* #MongoDB\Field(type="string",name="domain")
**/
private $domain;
/**
* #MongoDB\Field(type="date",name="created_at")
**/
private \DateTime $createdAt;
public function __construct()
{
$this->company = new Company();
$this->createdAt = new \DateTime();
}
/**
* #return mixed
*/
public function getCompany()
{
return $this->company;
}
/**
* #param mixed $company
*/
public function setCompany($company): void
{
$this->company = $company;
}
/**
* #return mixed
*/
public function getRealm()
{
return $this->realm;
}
/**
* #param mixed $realm
*/
public function setRealm($realm): void
{
$this->realm = $realm;
}
/**
* #return mixed
*/
public function getDomain()
{
return $this->domain;
}
/**
* #param mixed $domain
*/
public function setDomain($domain): void
{
$this->domain = $domain;
}
/**
* #return \DateTime
*/
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #param string $id
*/
public function setId(string $id): void
{
$this->id = $id;
}
}
Company embed document:
<?php
namespace App\Document\Entity;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
/** #MongoDB\EmbeddedDocument * */
class Company
{
/**
* #MongoDB\Field(type="string",name="company_id")
**/
private string $companyId;
/**
* #MongoDB\Field(type="int",name="company_version")
**/
private int $companyVersion;
/**
* #MongoDB\Field(type="string",name="company_name")
**/
private string $companyName;
/**
* #MongoDB\EmbedOne(targetDocument=Address::class)
*/
private Address $address;
/**
* #MongoDB\Field(type="string",name="email_address")
**/
private string $emailAddress;
/**
* #MongoDB\Field(type="string",name="phone_number")
**/
private string $phoneNumber;
/**
* #MongoDB\Field(type="string",name="website_url")
**/
private string $websiteUrl;
/**
* #MongoDB\Field(type="date",name="creation_date")
**/
private \DateTime $creationDate;
public function __construct()
{
$this->address = new Address();
}
public function getCompanyId(): string
{
return $this->companyId;
}
public function setCompanyId($companyId)
{
$this->companyId = $companyId;
}
public function getCompanyVersion(): int
{
return $this->companyVersion;
}
public function setCompanyVersion($companyVersion)
{
$this->companyVersion = $companyVersion;
}
public function getCreationDate(): \DateTime
{
return $this->creationDate;
}
public function setCreationDate($creationDate)
{
$this->creationDate = $creationDate;
}
public function getWebsiteUrl(): string
{
return $this->websiteUrl;
}
public function setWebsiteUrl($websiteUrl)
{
$this->websiteUrl = $websiteUrl;
}
public function getPhoneNumber(): string
{
return $this->phoneNumber;
}
public function setPhoneNumber($phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function getEmailAddress(): string
{
return $this->emailAddress;
}
public function setEmailAddress($emailAddress)
{
$this->emailAddress = $emailAddress;
}
public function getAddress(): Address
{
return $this->address;
}
public function setAddress(Address $address)
{
$this->address = $address;
}
public function getCompanyName(): string
{
return $this->companyName;
}
public function setCompanyName($companyName)
{
$this->companyName = $companyName;
}
}
What you're trying to do is perfectly fine from ODM's perspective - you can have as many embeddables deep as you want. Unless there is something fishy going on in your Address or Coordinates embedded documents I would expect a bug in the laminas-hydrator package. The fact that ORM does not allow nested embeddables makes this scenario more likely. Best would be to try creating a failing test case and send a pull request with it.
In the meantime you can leverage ODM's hydrator:
$hydrator = $this->dm->getHydratorFactory()->getHydratorFor(Account::class);
$hydrator->hydrate(new Account(), $data, [Query::HINT_READ_ONLY => true]);
Please note the Query::HINT_READ_ONLY passed as a 3rd argument to hydrate. With this hint ODM will not mark hydrated objects as managed which is what you need for later insertion. Hydrated EmbedOne objects should be good to go without the hint, but EmbedMany may not work correctly without it. Please see https://github.com/doctrine/mongodb-odm/issues/1377 and https://github.com/doctrine/mongodb-odm/pull/1403 for more details about said hint.
Thank you #malarzm. I do this:
$coordinates = new Coordinates();
$hydrator->hydrate($data['company']['address']['coordinates'], $coordinates);
unset($data['company']['address']['coordinates']);
$address = new Address();
$hydrator->hydrate($data['company']['address'], $address);
unset($data['company']['address']);
$company = new Company();
$hydrator->hydrate($data['company'], $company);
unset($data['company']);
$account = new Account();
$hydrator->hydrate($data, $account);
$address->setCoordinates($coordinates);
$company->setAddress($address);
$account->setCompany($company);
I realized unset() for each embed sud-document and finally I set each sub-document with set method of my entity.
It was the only way to do work fine.

Zend Expressive API does not return contents of objects

I'm creating a small API, mostly for learning purposes, but, I might implement it into a project I'm working on. So far, I have installed the zend expressive skeleton application and set up my models and entities. I'm able to query the database and get results, but, when I return the results as a JSON Response, I can only see a list of empty arrays for each result. I would like to be able to return the actual objects that are being returned from the database instead of converting them to arrays.
HomePageHandler.php
<?php
declare(strict_types=1);
namespace App\Handler;
use App\Entity\Product;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Template\TemplateRendererInterface;
use App\Model\ProductModel;
class HomePageHandler implements RequestHandlerInterface
{
/** #var string */
private $containerName;
/** #var Router\RouterInterface */
private $router;
/** #var null|TemplateRendererInterface */
private $template;
private $productModel;
public function __construct(
string $containerName,
Router\RouterInterface $router,
?TemplateRendererInterface $template = null,
ProductModel $productModel
) {
$this->containerName = $containerName;
$this->router = $router;
$this->template = $template;
$this->productModel = $productModel;
}
public function handle(ServerRequestInterface $request) : ResponseInterface
{
$data = $this->productModel->fetchAllProducts();
return new JsonResponse([$data]);
//return new HtmlResponse($this->template->render('app::home-page', $data));
}
}
I'm expecting a JSON Response returned with a list of 18 "Product" entities. My results look like.
[[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]]
Let me know if there is any other code you would like to see.
Thanks in advance!
Edited with Product.php code
<?php
/**
* Created by PhpStorm.
* User: Brock H. Caldwell
* Date: 3/14/2019
* Time: 4:04 PM
*/
namespace App\Entity;
class Product
{
protected $id;
protected $picture;
protected $shortDescription;
protected $longDescription;
protected $dimensions;
protected $price;
protected $sold;
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
/**
* #param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getPicture()
{
return $this->picture;
}
/**
* #param mixed $picture
*/
public function setPicture($picture)
{
$this->picture = $picture;
}
/**
* #return mixed
*/
public function getShortDescription()
{
return $this->shortDescription;
}
/**
* #param mixed $shortDescription
*/
public function setShortDescription($shortDescription)
{
$this->shortDescription = $shortDescription;
}
/**
* #return mixed
*/
public function getLongDescription()
{
return $this->longDescription;
}
/**
* #param mixed $longDescription
*/
public function setLongDescription($longDescription)
{
$this->longDescription = $longDescription;
}
/**
* #return mixed
*/
public function getDimensions()
{
return $this->dimensions;
}
/**
* #param mixed $dimensions
*/
public function setDimensions($dimensions)
{
$this->dimensions = $dimensions;
}
/**
* #return mixed
*/
public function getPrice()
{
return $this->price;
}
/**
* #param mixed $price
*/
public function setPrice($price)
{
$this->price = $price;
}
/**
* #return mixed
*/
public function getSold()
{
return $this->sold;
}
/**
* #param mixed $sold
*/
public function setSold($sold)
{
$this->sold = $sold;
}
public function exchangeArray($data)
{
$this->id = (!empty($data['id'])) ? $data['id'] : null;
$this->picture = (!empty($data['picture'])) ? $data['picture'] : null;
$this->shortDescription = (!empty($data['shortDescription'])) ? $data['shortDescription'] : null;
$this->longDescription = (!empty($data['longDescription'])) ? $data['longDescription'] : null;
$this->dimensions = (!empty($data['dimensions'])) ? $data['dimensions'] : null;
$this->price = (!empty($data['price'])) ? $data['price'] : null;
$this->sold = (!empty($data['sold'])) ? $data['sold'] : null;
}
}
You need to either make the properties public, or implement the JsonSerializable interface in your Product entity. All of its properties are protected, which is fine, but that means they aren't exposed when the object is JSON encoded.
Here are some brief examples:
class Example1 { protected $a = 1; }
class Example2 { public $b = 2; }
class Example3 implements JsonSerializable {
protected $c = 3;
public function jsonSerialize() {
return ['c' => $this->c];
}
}
echo json_encode([new Example1, new Example2, new Example3]);
The result:
[{},{"b":2},{"c":3}]
If you choose to implement JsonSerializable, exactly how you do it is up to you, but you just need a jsonSerialize() method that returns the properties you want in the JSON result in a format accessible to json_encode (an object with public properties or an array).

Argument 1 passed to __construct() must be an instance of GuzzleHttp\Client

I'm trying to "use" a vendor script to connect to feefo api (an online reviews service) but when I try and use the script it gives me this error:
Type error: Argument 1 passed to
BlueBayTravel\Feefo\Feefo::__construct() must be an instance of
GuzzleHttp\Client, null given, called in/Users/webuser1/Projects/_websites/domain.co.uk/plugins/gavinfoster/feefo/components/Feedback.php on line 47
Here is the vendor code I'm using:
/*
* This file is part of Feefo.
*
* (c) Blue Bay Travel <developers#bluebaytravel.co.uk>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace BlueBayTravel\Feefo;
use ArrayAccess;
use Countable;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Support\Arrayable;
use SimpleXMLElement;
/**
* This is the feefo class.
*
* #author James Brooks <james#bluebaytravel.co.uk>
*/
class Feefo implements Arrayable, ArrayAccess, Countable
{
/**
* The guzzle client.
*
* #var \GuzzleHttp\Client
*/
protected $client;
/**
* The config repository.
*
* #var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* The review items.
*
* #var array
*/
protected $data;
/**
* Create a new feefo instance.
*
* #param \GuzzleHttp\Client $client
* #param \Illuminate\Contracts\Config\Repository $config
*
* #return void
*/
public function __construct(Client $client, Repository $config)
{
$this->client = $client;
$this->config = $config;
}
/**
* Fetch feedback.
*
* #param array|null $params
*
* #return \BlueBayTravel\Feefo\Feefo
*/
public function fetch($params = null)
{
if ($params === null) {
$params['json'] = true;
$params['mode'] = 'both';
}
$params['logon'] = $this->config->get('feefo.logon');
$params['password'] = $this->config->get('feefo.password');
try {
$body = $this->client->get($this->getRequestUrl($params));
return $this->parse((string) $body->getBody());
} catch (Exception $e) {
throw $e; // Re-throw the exception
}
}
/**
* Parses the response.
*
* #param string $data
*
* #return \Illuminate\Support\Collection
*/
protected function parse($data)
{
$xml = new SimpleXMLElement($data);
foreach ((array) $xml as $items) {
if (isset($items->TOTALRESPONSES)) {
continue;
}
foreach ($items as $item) {
$this->data[] = new FeefoItem((array) $item);
}
}
return $this;
}
/**
* Assigns a value to the specified offset.
*
* #param mixed $offset
* #param mixed $value
*
* #return void
*/
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->data[] = $value;
} else {
$this->data[$offset] = $value;
}
}
/**
* Whether or not an offset exists.
*
* #param mixed $offset
*
* #return bool
*/
public function offsetExists($offset)
{
return isset($this->data[$offset]);
}
/**
* Unsets an offset.
*
* #param mixed $offset
*
* #return void
*/
public function offsetUnset($offset)
{
if ($this->offsetExists($offset)) {
unset($this->data[$offset]);
}
}
/**
* Returns the value at specified offset.
*
* #param mixed $offset
*
* #return mixed
*/
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->data[$offset] : null;
}
/**
* Count the number of items in the dataset.
*
* #return int
*/
public function count()
{
return count($this->data);
}
/**
* Get the instance as an array.
*
* #return array
*/
public function toArray()
{
return $this->data;
}
/**
* Returns the Feefo API endpoint.
*
* #param array $params
*
* #return string
*/
protected function getRequestUrl(array $params)
{
$query = http_build_query($params);
return sprintf('%s?%s', $this->config->get('feefo.baseuri'), $query);
}
}
And here is the code I'm using to try and use the fetch() method from the vendor class:
use Cms\Classes\ComponentBase;
use ArrayAccess;
use Countable;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Support\Arrayable;
use SimpleXMLElement;
use BlueBayTravel\Feefo\Feefo;
class Feedback extends ComponentBase
{
public $client;
public $config;
/**
* Container used for display
* #var BlueBayTravel\Feefo
*/
public $feedback;
public function componentDetails()
{
return [
'name' => 'Feedback Component',
'description' => 'Adds Feefo feedback to the website'
];
}
public function defineProperties()
{
return [];
}
public function onRun()
{
$this->feedback = $this->page['feedback'] = $this->loadFeedback($this->client, $this->config);
}
public function loadFeedback($client, $config)
{
$feefo = new Feefo($client, $config);
$feedback = $feefo->fetch();
return $feedback;
}
}
Won't allow me to call the Fetch() method statically so trying to instantiate and then use:
public function loadFeedback($client, $config)
{
$feefo = new Feefo($client, $config);
$feedback = $feefo->fetch();
return $feedback;
}
I've also tried type hinting the args like this:
public function loadFeedback(Client $client, Repository $config)
{
$feefo = new Feefo($client, $config);
$feedback = $feefo->fetch();
return $feedback;
}
But still I get the exception error above. I'm struggling to understand how to get past this. Any help for a newbie much appreciated :)
Just type hinting the function won't cast it to that object type. You need to properly pass the Guzzle\Client object to your function call.
// Make sure you 'use' the GuzzleClient on top of the class
// or use the Fully Qualified Class Name of the Client
$client = new Client();
$feedback = new Feedback();
// Now we passed the Client object to the function of the feedback class
// which will lead to the constructor of the Feefo class which is
// where your error is coming from.
$loadedFeedback = $feedback->loadFeedback($client);
Don't forget to do the same for the Repository $config from Laravel/Lumen

Symfony 2 - keeping the code DRY - FOSRestBundle

I'm currently using Symfony2 to create (and learn how to) a REST API. I'm using FOSRestBundle and i've created an "ApiControllerBase.php" with the following :
<?php
namespace Utopya\UtopyaBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ApiControllerBase
*
* #package Utopya\UtopyaBundle\Controller
*/
abstract class ApiControllerBase extends FOSRestController
{
/**
* #param string $entityName
* #param string $entityClass
*
* #return array
* #throws NotFoundHttpException
*/
protected function getObjects($entityName, $entityClass)
{
$dataRepository = $this->container->get("doctrine")->getRepository($entityClass);
$entityName = $entityName."s";
$data = $dataRepository->findAll();
foreach ($data as $object) {
if (!$object instanceof $entityClass) {
throw new NotFoundHttpException("$entityName not found");
}
}
return array($entityName => $data);
}
/**
* #param string $entityName
* #param string $entityClass
* #param integer $id
*
* #return array
* #throws NotFoundHttpException
*/
protected function getObject($entityName, $entityClass, $id)
{
$dataRepository = $this->container->get("doctrine")->getRepository($entityClass);
$data = $dataRepository->find($id);
if (!$data instanceof $entityClass) {
throw new NotFoundHttpException("$entityName not found");
}
return array($entityClass => $data);
}
/**
* #param FormTypeInterface $objectForm
* #param mixed $object
* #param string $route
*
* #return Response
*/
protected function processForm(FormTypeInterface $objectForm, $object, $route)
{
$statusCode = $object->getId() ? 204 : 201;
$em = $this->getDoctrine()->getManager();
$form = $this->createForm($objectForm, $object);
$form->submit($this->container->get('request_stack')->getCurrentRequest());
if ($form->isValid()) {
$em->persist($object);
$em->flush();
$response = new Response();
$response->setStatusCode($statusCode);
// set the `Location` header only when creating new resources
if (201 === $statusCode) {
$response->headers->set('Location',
$this->generateUrl(
$route, array('id' => $object->getId(), '_format' => 'json'),
true // absolute
)
);
}
return $response;
}
return View::create($form, 400);
}
}
This handles getting one object with a given id, all objects and process a form. But to use this i have to create as many controller as needed. By example : GameController.
<?php
namespace Utopya\UtopyaBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Utopya\UtopyaBundle\Entity\Game;
use Utopya\UtopyaBundle\Form\GameType;
/**
* Class GameController
*
* #package Utopya\UtopyaBundle\Controller
*/
class GameController extends ApiControllerBase
{
private $entityName = "Game";
private $entityClass = 'Utopya\UtopyaBundle\Entity\Game';
/**
* #Rest\View()
*/
public function getGamesAction()
{
return $this->getObjects($this->entityName, $this->entityClass);
}
/**
* #param int $id
*
* #return array
* #throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* #Rest\View()
*/
public function getGameAction($id)
{
return $this->getObject($this->entityName, $this->entityClass, $id);
}
/**
* #return mixed
*/
public function postGameAction()
{
return $this->processForm(new GameType(), new Game(), "get_game");
}
}
This sound not so bad to me but there's a main problem : if i want to create another controller (by example Server or User or Character), i'll have to do the same process and i don't want to since it'll be the same logic.
Another "maybe" problem could be my $entityName and $entityClass.
Any idea or could i make this better ?
Thank-you !
===== Edit 1 =====
I think i made up my mind. For those basics controllers. I would like to be able to "configure" instead of "repeat".
By example i could make a new node in config.yml with the following :
#config.yml
mynode:
game:
entity: 'Utopya\UtopyaBundle\Entity\Game'
This is a very basic example but is it possible to make this and transform it into my GameController with 3 methods routes (getGame, getGames, postGame) ?
I just want some leads if i can really achieve with this way or not, if yes with what components ? (Config, Router, etc.)
If no, what could i do? :)
Thanks !
I'd like to show my approach here.
I've created a base controller for the API, and I stick with the routing that's generated with FOSRest's rest routing type. Hence, the controller looks like this:
<?php
use FOS\RestBundle\Controller\Annotations\View;
use \Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\FormFactoryInterface,
Symfony\Bridge\Doctrine\RegistryInterface,
Symfony\Component\Security\Core\SecurityContextInterface,
Doctrine\Common\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
abstract class AbstractRestController
{
/**
* #var \Symfony\Component\Form\FormFactoryInterface
*/
protected $formFactory;
/**
* #var string
*/
protected $formType;
/**
* #var string
*/
protected $entityClass;
/**
* #var SecurityContextInterface
*/
protected $securityContext;
/**
* #var RegistryInterface
*/
protected $doctrine;
/**
* #var EventDispatcherInterface
*/
protected $dispatcher;
/**
* #param FormFactoryInterface $formFactory
* #param RegistryInterface $doctrine
* #param SecurityContextInterface $securityContext
* #param LoggerInterface $logger
*/
public function __construct(
FormFactoryInterface $formFactory,
RegistryInterface $doctrine,
SecurityContextInterface $securityContext,
EventDispatcherInterface $dispatcher
)
{
$this->formFactory = $formFactory;
$this->doctrine = $doctrine;
$this->securityContext = $securityContext;
$this->dispatcher = $dispatcher;
}
/**
* #param string $formType
*/
public function setFormType($formType)
{
$this->formType = $formType;
}
/**
* #param string $entityClass
*/
public function setEntityClass($entityClass)
{
$this->entityClass = $entityClass;
}
/**
* #param null $data
* #param array $options
*
* #return \Symfony\Component\Form\FormInterface
*/
public function createForm($data = null, $options = array())
{
return $this->formFactory->create(new $this->formType(), $data, $options);
}
/**
* #return RegistryInterface
*/
public function getDoctrine()
{
return $this->doctrine;
}
/**
* #return \Doctrine\ORM\EntityRepository
*/
public function getRepository()
{
return $this->doctrine->getRepository($this->entityClass);
}
/**
* #param Request $request
*
* #View(serializerGroups={"list"}, serializerEnableMaxDepthChecks=true)
*/
public function cgetAction(Request $request)
{
$this->logger->log('DEBUG', 'CGET ' . $this->entityClass);
$offset = null;
$limit = null;
if ($range = $request->headers->get('Range')) {
list($offset, $limit) = explode(',', $range);
}
return $this->getRepository()->findBy(
[],
null,
$limit,
$offset
);
}
/**
* #param int $id
*
* #return object
*
* #View(serializerGroups={"show"}, serializerEnableMaxDepthChecks=true)
*/
public function getAction($id)
{
$this->logger->log('DEBUG', 'GET ' . $this->entityClass);
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
return $object;
}
/**
* #param Request $request
*
* #return \Symfony\Component\Form\Form|\Symfony\Component\Form\FormInterface
*
* #View()
*/
public function postAction(Request $request)
{
$object = new $this->entityClass();
$form = $this->createForm($object);
$form->submit($request);
if ($form->isValid()) {
$this->doctrine->getManager()->persist($object);
$this->doctrine->getManager()->flush($object);
return $object;
}
return $form;
}
/**
* #param Request $request
* #param int $id
*
* #return \Symfony\Component\Form\FormInterface
*
* #View()
*/
public function putAction(Request $request, $id)
{
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
$form = $this->createForm($object);
$form->submit($request);
if ($form->isValid()) {
$this->doctrine->getManager()->persist($object);
$this->doctrine->getManager()->flush($object);
return $object;
}
return $form;
}
/**
* #param Request $request
* #param $id
*
* #View()
*
* #return object|\Symfony\Component\Form\FormInterface
*/
public function patchAction(Request $request, $id)
{
$this->logger->log('DEBUG', 'PATCH ' . $this->entityClass);
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
$form = $this->createForm($object);
$form->submit($request, false);
if ($form->isValid()) {
$this->doctrine->getManager()->persist($object);
$this->doctrine->getManager()->flush($object);
return $object;
}
return $form;
}
/**
* #param int $id
*
* #return array
*
* #View()
*/
public function deleteAction($id)
{
$this->logger->log('DEBUG', 'DELETE ' . $this->entityClass);
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
$this->doctrine->getManager()->remove($object);
$this->doctrine->getManager()->flush($object);
return ['success' => true];
}
}
It has methods for most of RESTful actions. Next, when I want to implement a CRUD for an entity, that's what I do:
The abstract controller is registered in the DI as an abstract service:
my_api.abstract_controller:
class: AbstractRestController
abstract: true
arguments:
- #form.factory
- #doctrine
- #security.context
- #logger
- #event_dispatcher
Next, when I'm implementing a CRUD for a new entity, I create an empty class and a service definition for it:
class SettingController extends AbstractRestController implements ClassResourceInterface {}
Note that the class implements ClassResourceInterface. This is necessary for the rest routing to work.
Here's the service declaration for this controller:
api.settings.controller.class: MyBundle\Controller\SettingController
api.settings.form.class: MyBundle\Form\SettingType
my_api.settings.controller:
class: %api.settings.controller.class%
parent: my_api.abstract_controller
calls:
- [ setEntityClass, [ MyBundle\Entity\Setting ] ]
- [ setFormType, [ %my_api.settings.form.class% ] ]
Then, I'm just including the controller in routing.yml as the FOSRestBundle doc states, and it's done.

Zend Authentication where user role stored in separate db table

I have a User table where userID, username and password are stored and a Role table which contains user role . To link these two tables, I have a table (user_role) which contains userID and roleID. How can I use Zend Auth to authenticate users and use Zend Acl to control user access. This is the database design
You can create a Zend_Auth adapter that works with whatever structure your application has.
Here is an example of an Auth adapter that uses my entity models and mappers to provide the credentials and user data for authentication.
<?php
/**
* Description of Auth_Adapter
*
*/
class Auth_Adapter implements Zend_Auth_Adapter_Interface
{
/**
* The username
*
* #var string
*/
protected $identity = null;
/**
* The password
*
* #var string
*/
protected $credential = null;
/**
* Users database object
*
* #var Model_Mapper_Abstract
*/
protected $usersMapper = null;
/**
* #param string $username
* #param string $password
* #param Model_Mapper_Abstract $userMapper
*/
public function __construct($username, $password, Model_Mapper_Abstract $userMapper = null)
{
if (!is_null($userMapper)) {
$this->setMapper($userMapper);
} else {
$this->usersMapper = new Application_Model_Mapper_User();
}
$this->setIdentity($username);
$this->setCredential($password);
}
/**
* #return \Zend_Auth_Result
*/
public function authenticate()
{
// Fetch user information according to username
$user = $this->getUserObject();
if (is_null($user)) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND,
$this->getIdentity(),
array('Invalid username')
);
}
// check whether or not the hash matches using my own password class
$check = Password::comparePassword($this->getCredential(), $user->password);
if (!$check) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
$this->getIdentity(),
array('Incorrect password')
);
}
// Success!
return new Zend_Auth_Result(
Zend_Auth_Result::SUCCESS,
$this->getIdentity(),
array()
);
}
/**
* #param type $userName
* #return \Auth_Adapter
*/
public function setIdentity($userName)
{
$this->identity = $userName;
return $this;
}
/**
* #param type $password
* #return \Auth_Adapter
*/
public function setCredential($password)
{
$this->credential = $password;
return $this;
}
/**
* #param type $mapper
* #return \Auth_Adapter
*/
public function setMapper($mapper)
{
$this->usersMapper = $mapper;
return $this;
}
/**
* #return object
*/
private function getUserObject()
{
return $this->getMapper()->findOneByColumn('name', $this->getIdentity());
}
/**
* #return object
*/
public function getUser()
{
$object = $this->getUserObject();
$array = array(
'id' => $object->id,
'name' => $object->name,
'role' => $object->role
);
return (object) $array;
}
/**
* #return string
*/
public function getIdentity()
{
return $this->identity;
}
/**
* #return string
*/
public function getCredential()
{
return $this->credential;
}
/**
* #return object Model_Mapper_Abstract
*/
public function getMapper()
{
return $this->usersMapper;
}
}
You could also extend any of the current adapters to provide the functionality you need.
Good Luck!

Categories