I want to validate the object properties. Because we do not know PHP OOP more, we chose this way:
First create Validator:
namespace Validator;
/**
* Class ObjectPropertyValidator - Validate object, test if have all property
* #package Validator
*/
class ObjectPropertyValidator {
/**
* #param object $object
*
* #throws ValidatorException
*/
public static function validate($object) {
$reflectionClass = new \ReflectionClass(get_called_class());
foreach ($reflectionClass->getProperties() as $property) {
$name = $property->name;
if (!property_exists($object, $name)) {
throw new ValidatorException('Object not have property ' . $name, 0);
}
new PropertyValidator($property->getDocComment(), $object->$name, $name);
}
}
}
Next I am create Property Validator (I do not have all types validating):
namespace Validator;
/**
* Class PropertyValidator - Parse PHPDOC to pieces and validate input property
* #package Validator
*/
class PropertyValidator {
const DOC_PATTERN = '/#var\s+(?<object>\\\?)(?<type>[0-9A-Za-z\\\]+)(?<array>\[?\]?)/';
const DOC_IS_ARRAY = '[]';
const DOC_IS_OBJECT = '\\';
/** #var bool */
public $is_Array = false;
/** #var bool */
public $is_Object = false;
/** #var string */
public $type = '';
/**
* PropertyValidator constructor.
*
* Where make validating over PHPDOC over $property
*
* #param string $doc PHPDOC
* #param mixed $property Validating Property
* #param string $name Name of property
*/
public function __construct($doc, $property, $name) {
$this->analyzePHPDOC($doc);
$this->validate($property, $name);
}
/**
* #param string $doc
*/
private function analyzePHPDOC($doc) {
if (preg_match(self::DOC_PATTERN, $doc, $matches)) {
$this->is_Array = $matches['array'] === self::DOC_IS_ARRAY;
$this->type = $matches['object'] . $matches['type'];
$this->is_Object = $matches['object'] === self::DOC_IS_OBJECT;
}
}
/**
* #param object $property
* #param string $name
*
* #throws ValidatorException
*/
private function validate($property, $name) {
if ($this->is_Array) {
if (!is_array($property)) {
throw new ValidatorException('Object not have property ' . $name . ' type array', 0);
}
if ($this->is_Object) {
$validator = new $this->type();
foreach ($property as $subobject) {
/** #var ObjectPropertyValidator $validator */
$validator::validate($subobject);
}
}
} else {
if ($this->is_Object) {
$validator = new $this->type();
/** #var ObjectPropertyValidator $validator */
$validator::validate($property);
} else {
switch ($this->type) {
case 'string':
if (!is_string($property)) {
throw new ValidatorException('Property ' . $name . ' is not string', 0);
}
break;
case 'int':
case 'integer':
if (!is_numeric($property)) {
throw new ValidatorException('Property ' . $name . ' is not numeric', 0);
}
break;
}
}
}
}
}
Here I have a skeleton classes for validation (in separate files):
namespace Object;
use Validator\ObjectPropertyValidator;
class SkeletonForValidatingA extends ObjectPropertyValidator {
/** #var string */
public $someProperty;
/** #var \Object\SkeletonForValidatingB[] */
public $somePropertyArray;
}
namespace Object;
use Validator\ObjectPropertyValidator;
class SkeletonForValidatingB extends ObjectPropertyValidator {
/** #var int*/
public $someProperty;
}
And last how to I using all:
$parameters = '{"someProperty":"some text", "somePropertyArray":[{"someProperty":1}]}';
$parameters = Zend_Json::decode($parameters, false);
SkeletonForValidatingA::validate($parameters);
If anyone has any idea how to do it better, please send a tip!
Thank you all
Related
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
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);
}
}
?>
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.
Consider that I got several models that store some flags using MySQL's bit field.
I know that it can easily be converted into bool by doing the follow:
$myBoolFlag = (ord($model->myFlag) == 1) ? true : false;
I'm looking for some way to make it map to a boolean property automatically.
Please someone tell me that there is an better way than creating setters and getters for every bit field in my project. I bet that Phalcon has some magic configuration in the DB service or something like this...
The way I have dealt with a similar issue is to create map arrays in each model and work with those. More specifically:
<?php
/**
* Model.php
*
* Model
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2012-12-12
* #category Library
*
*/
namespace NDN;
use \Phalcon\DI\FactoryDefault as PhDi;
use \Phalcon\Mvc\Model as PhModel;
class Model extends PhModel
{
private $meta = [];
/**
* Some init stuff
*/
public function initialize()
{
// Disable literals
$this->setup(['phqlLiterals' => false]);
}
/**
* Universal method caller. This checks the available methods based on
* the fields in the meta array and returns the relevant results
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2015-02-15
*
* #param string $function
* #param array|null $arguments
*
* #return mixed|void
* #throws \Exception
*/
public function __call($function, $arguments = null)
{
// $function is something like getId, setId, getName etc.
$metaFunction = substr($function, 3);
$field = $this->getMetaFunctionToField($metaFunction);
if ($field) {
$prefix = substr($function, 0, 3);
$fieldName = $field['field'];
switch ($prefix) {
case 'get':
/**
* Data manipulation here if needed
*/
$value = $this->getField($fieldName);
$value = $this->metaFieldValidate($field, $value);
return $value;
break;
case 'set':
/**
* Data manipulation here
*/
$value = $this->metaFieldValidate($field, $arguments);
$this->setField($field, $value);
break;
}
} else {
throw new \Exception('Function does not exist');
}
}
/**
* -------------------------------------------------------------------------
* PROTECTED METHODS
* -------------------------------------------------------------------------
*/
/**
* Gets a field from the model with the correct prefix
*
* #param $name
*
* #return mixed
*/
protected function getField($name)
{
return $this->$name;
}
/**
* Sets a field in the model
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2014-02-15
*
* #param string $field
* #param mixed $value
*/
protected function setField($field, $value)
{
$this->$field = $value;
}
/**
* Returns the DI container
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2014-02-22
*
* #return mixed
*/
public function getDI()
{
return PhDi::getDefault();
}
/**
* Accesses the internal array map to provide the field name from a function
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2014-02-27
*
* #param string $prefix The prefix of the table
* #param string $function The aliased function
*
* #return string The field name (i.e. tnt_id)
* bool False if not found
*/
public function getMetaFunctionToField($function)
{
if (array_key_exists($function, $this->meta)) {
return $this->meta[$function];
}
return false;
}
/**
* Validates a setter value based on each field's type
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2014-02-17
*
* #param string $field The field to check
* #param mixed $value The value of the field
*
* #return bool|int|string
*/
protected function metaFieldValidate($field, $value)
{
// Find the validator
$validator = $field['validator'];
switch ($validator)
{
case 'int':
$return = intval($value);
break;
case 'bit':
$return = (ord($value) == 1) ? true : false;
break;
case 'bool':
$return = (bool) $value;
break;
case 'decimal':
$return = (float) $value;
break;
case 'string':
$return = (string) $value;
break;
case 'datetime':
/**
* #todo check datetime validator
*/
$return = (string) $value;
break;
default:
$return = $value;
break;
}
return $return;
}
}
A sample User model looks like this
<?php
/**
* User.php
*
* User
*
* #author Nikos Dimopoulos <nikos#niden.net>
* #since 2014-03-08
* #category Models
*
*/
namespace NDN;
use \NDN\Model as NDNModel;
class Model extends NDNModel
{
public function initialize()
{
/**
* This is where I will set the field map
*
* The key of the array is the function name without
* the prefix. So for instance if you want getName()
* to return the user.name you use Name as the key
*/
$this->data = [
'Id' => [
'field' => 'user_id',
'validator' => 'int',
],
'Name' => [
'field' => 'user_name',
'validator' => 'int',
],
'IsMarried' => [
'field' => 'user_is_married',
'validator' => 'bit',
]
];
parent::initialize();
}
}
I've ended up by following the Nikolao's approach and implemented a base class for my models that can parse bit fields into more practical values. However, I designed it to not override the default mapping behavior. Something like that:
abstract class BaseModel extends Phalcon\Mvc\Model
{
public function explicitDataTypes()
{
//TODO: Suport relational data
$numargs = func_num_args();
if($numargs)
{
foreach (func_get_args() as $arg)
{
if(isset($this->$arg)) $this->$arg = $this->explicitDataType($arg)
}
}
else
{
foreach (get_object_vars($this) as $key => $value)
{
if($key[0] != '_') $this->$key = $this->explicitDataType($key)
}
}
}
public function explicitDataType($propertyName)
{
$value = $this->$propertyName;
if(is_numeric($value))
{
$locale = localeconv();
$separatorCount = substr_count($value, $locale['decimal_point']);
if($separatorCount == 0) $value = (int)$value;
elseif($separatorCount == 1) $value = (float)$value;
}
elseif(strlen($value) == 1 && ord($value) <= 1) $value = ord($value) == 1;
return $value;
}
}
Examples
class User extends BaseModel
{
protected $password;
public function afterFetch()
{
//Explicit data type for specific fields (e.g. var_dump($user->activated); //bool(true))
$this->explicitDataTypes('activated', 'deleted');
}
}
class Location extends BaseModel
{
public function afterFetch()
{
//Explicit data type for specific all fields (e.g. var_dump($location->distance); //float(12.3)
$this->explicitDataTypes();
}
}
Simple question, how do I convert an associative array to variables in a class? I know there is casting to do an (object) $myarray or whatever it is, but that will create a new stdClass and doesn't help me much. Are there any easy one or two line methods to make each $key => $value pair in my array into a $key = $value variable for my class? I don't find it very logical to use a foreach loop for this, I'd be better off just converting it to a stdClass and storing that in a variable, wouldn't I?
class MyClass {
var $myvar; // I want variables like this, so they can be references as $this->myvar
function __construct($myarray) {
// a function to put my array into variables
}
}
This simple code should work:
<?php
class MyClass {
public function __construct(Array $properties=array()){
foreach($properties as $key => $value){
$this->{$key} = $value;
}
}
}
?>
Example usage
$foo = new MyClass(array("hello" => "world"));
$foo->hello // => "world"
Alternatively, this might be a better approach
<?php
class MyClass {
private $_data;
public function __construct(Array $properties=array()){
$this->_data = $properties;
}
// magic methods!
public function __set($property, $value){
return $this->_data[$property] = $value;
}
public function __get($property){
return array_key_exists($property, $this->_data)
? $this->_data[$property]
: null
;
}
}
?>
Usage is the same
// init
$foo = new MyClass(array("hello" => "world"));
$foo->hello; // => "world"
// set: this calls __set()
$foo->invader = "zim";
// get: this calls __get()
$foo->invader; // => "zim"
// attempt to get a data[key] that isn't set
$foo->invalid; // => null
Best solution is to have trait with static function fromArray that can be used for data loading:
trait FromArray {
public static function fromArray(array $data = []) {
foreach (get_object_vars($obj = new self) as $property => $default) {
if (!array_key_exists($property, $data)) continue;
$obj->{$property} = $data[$property]; // assign value to object
}
return $obj;
}
}
Then you can use this trait like that:
class Example {
use FromArray;
public $data;
public $prop;
}
Then you can call static fromArray function to get new instance of Example class:
$obj = Example::fromArray(['data' => 123, 'prop' => false]);
var_dump($obj);
I also have much more sophisticated version with nesting and value filtering https://github.com/OzzyCzech/fromArray
if you (like me) came here looking for a source code generator for array->class, i couldn't really find any, and then i came up with this (a work in progress, not well tested or anything, json_decode returns the array.):
<?php
declare(strict_types = 1);
$json = <<<'JSON'
{"object_kind":"push","event_name":"push","before":"657dbca6668a99012952c58e8c8072d338b48d20","after":"5ac3eda70dbb44bfdf98a3db87515864036db0f9","ref":"refs/heads/master","checkout_sha":"5ac3eda70dbb44bfdf98a3db87515864036db0f9","message":null,"user_id":805411,"user_name":"hanshenrik","user_email":"divinity76#gmail.com","user_avatar":"https://secure.gravatar.com/avatar/e3af2bd4b5604b0b661b5e6646544eba?s=80\u0026d=identicon","project_id":3498684,"project":{"name":"gitlab_integration_tests","description":"","web_url":"https://gitlab.com/divinity76/gitlab_integration_tests","avatar_url":null,"git_ssh_url":"git#gitlab.com:divinity76/gitlab_integration_tests.git","git_http_url":"https://gitlab.com/divinity76/gitlab_integration_tests.git","namespace":"divinity76","visibility_level":0,"path_with_namespace":"divinity76/gitlab_integration_tests","default_branch":"master","homepage":"https://gitlab.com/divinity76/gitlab_integration_tests","url":"git#gitlab.com:divinity76/gitlab_integration_tests.git","ssh_url":"git#gitlab.com:divinity76/gitlab_integration_tests.git","http_url":"https://gitlab.com/divinity76/gitlab_integration_tests.git"},"commits":[{"id":"5ac3eda70dbb44bfdf98a3db87515864036db0f9","message":"dsf\n","timestamp":"2017-06-14T02:21:50+02:00","url":"https://gitlab.com/divinity76/gitlab_integration_tests/commit/5ac3eda70dbb44bfdf98a3db87515864036db0f9","author":{"name":"hanshenrik","email":"divinity76#gmail.com"},"added":[],"modified":["gitlab_callback_page.php"],"removed":[]}],"total_commits_count":1,"repository":{"name":"gitlab_integration_tests","url":"git#gitlab.com:divinity76/gitlab_integration_tests.git","description":"","homepage":"https://gitlab.com/divinity76/gitlab_integration_tests","git_http_url":"https://gitlab.com/divinity76/gitlab_integration_tests.git","git_ssh_url":"git#gitlab.com:divinity76/gitlab_integration_tests.git","visibility_level":0}}
JSON;
$arr = json_decode ( $json, true );
var_dump ( array_to_class ( $arr ) );
/**
*
* #param array $arr
* #param string $top_class_name
*/
function array_to_class(array $arr, string $top_class_name = "TopClass"): string {
$top_class_name = ucfirst ( $top_class_name );
$classes = array (); // deduplicated 'definition'=>true,array_keys();
$internal = function (array $arr, string $top_class_name) use (&$classes, &$internal) {
$curr = 'Class ' . $top_class_name . ' {' . "\n";
foreach ( $arr as $key => $val ) {
$type = gettype ( $val );
if (is_array ( $val )) {
$type = ucfirst ( ( string ) $key );
$classes [$internal ( $val, ( string ) $key )] = true;
}
$curr .= <<<FOO
/**
* #property $type \$$key
*/
FOO;
$curr .= "\n public $" . $key . ";\n";
}
$curr .= '}';
$classes [$curr] = true;
};
$internal ( $arr, $top_class_name );
return implode ( "\n", array_keys ( $classes ) );
}
output:
Class project {
/**
* #property string $name
*/
public $name;
/**
* #property string $description
*/
public $description;
/**
* #property string $web_url
*/
public $web_url;
/**
* #property NULL $avatar_url
*/
public $avatar_url;
/**
* #property string $git_ssh_url
*/
public $git_ssh_url;
/**
* #property string $git_http_url
*/
public $git_http_url;
/**
* #property string $namespace
*/
public $namespace;
/**
* #property integer $visibility_level
*/
public $visibility_level;
/**
* #property string $path_with_namespace
*/
public $path_with_namespace;
/**
* #property string $default_branch
*/
public $default_branch;
/**
* #property string $homepage
*/
public $homepage;
/**
* #property string $url
*/
public $url;
/**
* #property string $ssh_url
*/
public $ssh_url;
/**
* #property string $http_url
*/
public $http_url;
}
Class author {
/**
* #property string $name
*/
public $name;
/**
* #property string $email
*/
public $email;
}
Class added {
}
Class modified {
/**
* #property string $0
*/
public $0;
}
Class removed {
}
Class 0 {
/**
* #property string $id
*/
public $id;
/**
* #property string $message
*/
public $message;
/**
* #property string $timestamp
*/
public $timestamp;
/**
* #property string $url
*/
public $url;
/**
* #property Author $author
*/
public $author;
/**
* #property Added $added
*/
public $added;
/**
* #property Modified $modified
*/
public $modified;
/**
* #property Removed $removed
*/
public $removed;
}
Class commits {
/**
* #property 0 $0
*/
public $0;
}
Class repository {
/**
* #property string $name
*/
public $name;
/**
* #property string $url
*/
public $url;
/**
* #property string $description
*/
public $description;
/**
* #property string $homepage
*/
public $homepage;
/**
* #property string $git_http_url
*/
public $git_http_url;
/**
* #property string $git_ssh_url
*/
public $git_ssh_url;
/**
* #property integer $visibility_level
*/
public $visibility_level;
}
Class TopClass {
/**
* #property string $object_kind
*/
public $object_kind;
/**
* #property string $event_name
*/
public $event_name;
/**
* #property string $before
*/
public $before;
/**
* #property string $after
*/
public $after;
/**
* #property string $ref
*/
public $ref;
/**
* #property string $checkout_sha
*/
public $checkout_sha;
/**
* #property NULL $message
*/
public $message;
/**
* #property integer $user_id
*/
public $user_id;
/**
* #property string $user_name
*/
public $user_name;
/**
* #property string $user_email
*/
public $user_email;
/**
* #property string $user_avatar
*/
public $user_avatar;
/**
* #property integer $project_id
*/
public $project_id;
/**
* #property Project $project
*/
public $project;
/**
* #property Commits $commits
*/
public $commits;
/**
* #property integer $total_commits_count
*/
public $total_commits_count;
/**
* #property Repository $repository
*/
public $repository;
}
Here's another solution using PDOStatement::fetchObject, though it is a bit of a hack.
$array = array('property1' => 'value1', 'property2' => 'value2');
$className = 'MyClass';
$pdo = new PDO('sqlite::memory:'); // we don't actually need sqlite; any PDO connection will do
$select = 'SELECT ? AS property1, ? AS property2'; // this could also be built from the array keys
$statement = $pdo->prepare($select);
// this last part can also be re-used in a loop
$statement->execute(array_values($array));
$myObject = $statement->fetchObject($className);
Define a static method to convert get an instance from an array. Best, define an interface for it. This is declarative, does not pollute the constructor, allows you to set private properties and still implement custom logic that would not be possible with Reflection. If you want a generic solution, define a trait and use it in your classes.
class Test implements ContructableFromArray {
private $property;
public static function fromArray(array $array) {
$instance = new self();
$instance->property = $array['property'];
return $instance;
}
}
interface ConstructableFromArray {
public static function fromArray(array $array);
}
If you want to cast nested array to object use this code:
class ToObject
{
private $_data;
public function __construct(array $data)
{
$this->setData($data);
}
/**
* #return array
*/
public function getData()
{
return $this->_data;
}
/**
* #param array $data
*/
public function setData(array $data)
{
$this->_data = $data;
return $this;
}
public function __call($property, $args)
{
// NOTE: change lcfirst if you need (ucfirst/...) or put all together
$property = lcfirst(str_replace('get', '', $property));
if (array_key_exists($property, $this->_data)) {
if (is_array($this->_data[$property])) {
return new self($this->_data[$property]);
}
return $this->_data[$property];
}
return null;
}
}
Then you can use like this:
$array = [
'first' => '1.1',
'second' => [
'first' => '2.1',
'second' => '2.2',
'third' => [
'first' => '2.3.1'
]
]
];
$object = new ToObject($array);
$object->getFirst(); // returns 1.1
$object->getSecond()->getFirst(); // returns 2.1
$object->getSecond()->getData(); // returns second array
$object->getSecond()->getThird()->getFirst(); // returns 2.3.1
Just use the php spread operator
class Whatever
{
public function __construct(public int $something) {}
public static function fromArray(array $data)
{
return new self(...$data);
}
}
$data = ['something' => 1];
$instance = Whatever::fromArray($data);
$instance->something; // equals 1