I can't understand why the data is not transmitted in the custom facade (Format) method parameter. I'm doing exception handling for the API and using my visualization handler (JsonExceptionFormatter).
Interface CanFormat:
interface CanFormat
{
public function format($data);
}
Facade Format:
class Format extends Facade
{
protected static function getFacadeAccessor(): string
{
return \Hopex\VkSdk\Foundation\Format::class;
}
}
Class Format:
namespace Hopex\VkSdk\Facades;
class Format implements CanFormat
{
private array $formatters = [];
public function with(string $formatter): static
{
$formatter = new $formatter();
if ($formatter instanceof CanFormat) {
$this->formatters[] = $formatter;
}
return $this;
}
public function format($data): mixed
{
foreach ($this->formatters as $formatter) {
$data = $formatter->format($data);
}
return $data;
}
}
Exception render method:
final public function render(Request $request)
{
if (env('LOG_LEVEL') === 'debug') {
dump($this->getMessage()); // added for test (next "dump 1")
return new JsonResponse(
Format::with(JsonExceptionFormatter::class)->format($this->getMessage()),
$this->getCode()
);
}
}
Visualization handler:
class JsonExceptionFormatter implements CanFormat
{
public function format($data): array
{
dump($data); // added for test (next "dump 2")
return [
'type' => 'error',
'message' => $data instanceof SdkException ? $data->getMessage() : $data
];
}
}
It's dumps:
[dump 1]: "ApiException: User authorization failed"
[dump 2]: []
P.S. Other formats work without problems.
Related
I have some PHP snippets for an application I am trying to restrict inputs coming from a request in the front end of my JavaScript application. The page sends a request using JSON object which contains a field value present that I assign as 'Open', 'Complete', or 'Closed'. I want to prevent unwanted input tampering or values to be sent through.
Question:
Below property $eventstatus is type hinted with the enum, but when I assign the string value inside $array['EventStatus'] PHP (7.4.9) reports an error that my types are not compatible. It needs to see a Status type when in fact I am assigning it a string.
How do I fix this?
$event->eventstatus = $array['EventStatus'];
Enum class (Status)
<?php
namespace app\enums;
abstract class Status
{
const Open = 'Open';
const Complete = 'Complete';
const Closed = 'Closed';
}
Mapper Class Member Function - snippet, code below takes an array value and maps it to a class property
<?php
function mapFromArray($event, $array) {
if (!is_null($array['EventStatus'])) $event->eventstatus = $array['EventStatus'];
}
Model Class
<?php
namespace data\model;
use app\enums\Status;
class Event
{
public $eventid;
public $riskid;
public $eventtitle;
public Status $eventstatus;
}
Your type hint actually tells PHP that you expect $eventstatus to be an instance of Status. But the values are actually just simple strings: 'Open', 'Complete' and 'Closed'.
So the correct type hint would be:
<?php
namespace data\model;
use app\enums\Status;
class Event
{
// ...
public string $eventstatus;
}
But with this PHP accepts any string and not only a "valid" one. Using proper Enums here would help but currently PHP 7 has no native support for Enums (which is implemented for PHP 8.1 though).
If you want to use the Status class for more readable code you can just change the type hint to string.
If you want to validate the input data you could extend the code like this:
<?php
namespace app\enums;
abstract class Status
{
const Open = 'Open';
const Complete = 'Complete';
const Closed = 'Closed';
const Valid_Statuses = [
self::Open,
self::Complete,
self::Closed,
];
}
function mapFromArray($event, $array) {
if (!is_null($array['EventStatus'])) {
if (in_array($array['EventStatus'], Status::Valid_Statuses)) {
$event->eventstatus = $array['EventStatus'];
} else {
// handle invalid status value here
}
}
}
If you want to use strict type hinting to ensure validity everywhere you'd need to wrap the value into a instance of the class, e.g.:
namespace app\enums;
abstract class Status
{
const Open = 'Open';
const Complete = 'Complete';
const Closed = 'Closed';
const Valid_Statuses = [
self::Open,
self::Complete,
self::Closed,
];
private string $value;
public function __construct(string $value) {
if (!in_array($value, self::Valid_Statuses)) {
throw \InvalidArgumentException(sprintf('Invalid status "%s"', $value));
}
$this->value = $value;
}
public function getValue(): string {
return $this->value;
}
public function __toString(): string {
return $this->value;
}
}
function mapFromArray($event, $array) {
if (!is_null($array['EventStatus'])) {
try {
$event->eventstatus = new Status($array['EventStatus']);
} catch (\Exception $exception) {
// handle invalid status value here
}
}
}
I tried a slightly different method from what was proposed using array values, but still relying on some sort of array to check for allowed values.
In my Events class I extended from abstract class Mapper (within which I added a new performMapping function to make mapping more dynamic)
<?php
namespace data\mapper;
use app\enums\Status;
use data\model\Event;
class Events extends Mapper
{
public function mapFromArray($array) : Event
{
$event = $this->_performMapping($array, new Event());
return $event;
}
}
Model - Added Magic Methods (__set, __get)
<?php
namespace data\model;
use app\enums\Status;
class Event
{
public $eventid;
public $riskid;
public $eventtitle;
private $eventstatus;
public $eventownerid;
public $actualdate;
public $scheduledate;
public $baselinedate;
public $actuallikelihood;
public $actualtechnical;
public $actualschedule;
public $actualcost;
public $scheduledlikelihood;
public $scheduledtechnical;
public $scheduledschedule;
public $scheduledcost;
public $baselinelikelihood;
public $baselinetechnical;
public $baselineschedule;
public $baselinecost;
public function __set($name, $value)
{
switch ($name)
{
case 'eventstatus':
{
$class = Status::class;
try
{
$reflection = new \ReflectionClass($class);
}
catch (\ReflectionException $ex)
{
return null;
}
$constants = $reflection->getConstants();
if (array_key_exists($value, $constants))
$this->$name = constant("\\".$class."::$constants[$value]");
else
throw (new \Exception("Property $name not found in " . $class));
}
default:
{
if (property_exists(get_class($this), $name))
$this->$name = $value;
else
throw (new \Exception("Property $name not found in " . get_class($this)));
}
}
}
public function __get($name)
{
switch ($name)
{
case 'eventstatus':
return $this->$name;
default:
if (property_exists($this, $name))
return $this->$name;
else
return null;
}
}
}
Mapper
<?php
namespace data\mapper;
abstract class mapper
{
protected $db = null;
public function __construct(\PDO $db)
{
$this->db = $db;
}
abstract public function mapFromArray($array);
protected function _populateFromCollection($results = null)
{
$return = [];
if ($results != null)
{
foreach($results as $result)
{
$return[] = $this->mapFromArray($result);
}
}
return $return;
}
protected function _performMapping($array, $object)
{
foreach (array_keys($array) as $property)
{
$lowerCaseProperty = strtolower($property);
if (property_exists(get_class($object), $property))
$object->$property = $array[$property];
else if (property_exists(get_class($object), $lowerCaseProperty))
$object->$lowerCaseProperty = $array[$property];
}
return $object;
}
Enum
<?php
namespace app\enums;
abstract class Status
{
const Open = 'Open';
const Complete = 'Complete';
const Closed = 'Closed';
}
I am using Maatwebsite\Excel to handle data exports in my application. Some of the exports are pretty big, so I want to queue the exports. I have followed the documentation, but I get the following error when attempting to export:
You cannot serialize or unserialize PDO instances
I understand that PDO instances cannot be serialized, I just don't understand why it's telling me that since I am following what is said in the docs. Here's my code:
Controller
$path = 'public/validations/' . $filename;
//the $client var is a record retrieved from a model, $input is the result of $request->all()
(new ValidationsExport($client, $input))->queue($path)->chain([
// the script never reaches here, but $user is from \Auth::user()
new NotifyUserOfValidationExport($user, $filename),
]);
Export
class ValidationsExport implements FromQuery, WithHeadings, WithMapping, WithStrictNullComparison, WithCustomQuerySize, WithEvents, WithColumnFormatting
{
use Exportable;
private $client;
private $input;
public function __construct($client, $input)
{
$this->client = $client;
$this->input = $input;
}
public function query()
{
// this does return a query builder object, but this is required for FromQuery and is shown in the docs as an example of how to queue an export
$this->validations = $this->getValidations();
return $this->validations;
}
public function querySize(): int
{
$query = ....
$size = $query->count();
return $size;
}
public function headings(): array
{
// this is an array
return $this->columns;
}
public function columnFormats(): array
{
return [
'A' => NumberFormat::FORMAT_TEXT,
'B' => NumberFormat::FORMAT_TEXT,
'C' => NumberFormat::FORMAT_TEXT
];
}
public function map($row): array
{
$mapping = [];
foreach($row as $key => $value) {
if(is_bool($value)) {
if($value) {
$mapping[$key] = "Yes";
} else {
$mapping[$key] = "No";
}
}else{
$mapping[$key] = $value;
}
}
return $mapping;
}
//.......
}
I assume that the problem comes from using FromQuery, but I can't use FromCollection because I run out of memory since the export is so big. I need the built in chunking that FromQuery uses. Is there a way I can queue an export using FromQuery?
You probably don't need to be setting a member variable $this->validations to that builder. That is what is ending up trying to be serialized. If you are just going to return it you don't need to store a copy on the class.
Am using fractal package from phpleague. I have a transform class setup like this
class ConversationTransformer extends TransformerAbstract
{
public function transform (Conversation $conversation, $user)
{
return [];
}
}
however i get missing argument 2 exception for transform when i try to access it
$user = $this->people->get($this->user());
//conversations
$conversations = $this->conversations->user($user);
return $this->fractal->paginatedCollection($conversations, $user, new ConversationTransformer());
class ConversationTransformer extends TransformerAbstract
{
private $params = [];
function __construct($params = [])
{
$this->params = $params;
}
public function transform (Conversation $conversation)
{
//-- $this->params will be used here
return [];
}
}
Call it with this return:
return $this->fractal->paginatedCollection($conversations, new ConversationTransformer(['user' => $user]))
Ok so currently have this function in controller, which is called multiple times.
public function formatFloat($value)
{
return (float)sprintf('%0.6f', $value);
}
So I am trying to use getters and setters so I can just use
$model->$whatever;
and the formatting will be done.
In my model I have
public function getChargePeak()
{
return $this->charge_peak;
}
public function setChargePeak($value)
{
return $this->charge_peak = (float)sprintf('%0.6f', $value);
}
but when doing
$peak = $model->chargepeak;
var_dump($peak);die;
it is still returning as a string
If the charge_peak property is stored as string and you need a float in you app you should use
public function getChargePeak()
{
return floatval($this->charge_peak);
}
Anyway you should store the values in a coherent way as you use the values in your app ..
http://php.net/manual/en/function.floatval.php
So I suggest u another pattern: decorator and helpers. You should use a controller only to get data from request, prepare it for model and send it to view.
Formatting values is a helper logic. So create a new class
\common\helpers\Number.php
namespace common\helpers;
class Number
{
public static function formatFloat($value)
{
return (float)sprintf('%0.6f', $value);
}
}
Then create decorator for your model:
namespace common\models\decorators;
class YourModelDecorator
{
/**
* YourModel
*/
private $model;
public function __construct(YourModel $model)
{
$this->model = $model;
}
public function __get($name)
{
$methodName = 'get' . $name;
if (method_exists(self::class, $methodName)) {
return $this->$methodName();
} else {
return $this->model->{$name};
}
}
public function __call($name, $arguments)
{
return $this->model->$name($arguments);
}
public function getChargePeak()
{
return \common\helpers\Number::formatFloat($this->model->charge_peak);
}
}
and send it to view for example:
public function actionView($id)
{
$model = $this->loadModel($id);
$this->render('view', [
'model' => new \common\models\decorators\YourModelDecorator($model)
]);
}
can somebody try to explain me how to use multiple normalizers when serializing data from multiple classes with the Symfony serializer?
Lets say that I have the following classes:
class User
{
private $name;
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// getters and setters
}
class Book
{
private $title;
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}
And I want to serialize an user who has multiple books.
$first = new Book();
$first->setTitle('First book');
$second = new Book();
$second->setTitle('Second book');
$user = new User();
$user->setName('Person name');
$user->addBook($first);
$user->addBook($second);
dump($this->get('serializer')->serialize($user, 'json'));
die();
Let's say that I also want to include a hash when serializing a book, so I have the following normalizer:
class BookNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
return [
'title' => $object->getTitle(),
'hash' => md5($object->getTitle())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Book;
}
}
And I am getting the expected result:
{"name":"Person name","books":[{"title":"First book","hash":"a9c04245e768bc5bedd57ebd62a6309e"},{"title":"Second book","hash":"c431a001cb16a82a937579a50ea12e51"}]}
The problem comes when I also add a normalizer for the User class:
class UserNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
return [
'name' => $object->getName(),
'books' => $object->getBooks()
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}
Now, the books aren't normalized using the previously given normalizer, and i get the following:
{"name":"Person name","books":[{},{}]}
I tried to find a way (documentation and other articles) to always call the normalizers for the given types (eg. always call the book normalizer when the type is Book, even if the data is nested and used in another normalizer) but could not succeed.
I think i have misunderstood something about normalizers but don't know what. Can somebody explain to is what i want possible and how to do it?
You have to use the NormalizerAwareTrait so you can access the normalizer for books
add interface
use trait
call normalize() method for books
code:
class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($object, $format = null, array $context = array())
{
return [
'name' => $object->getName(),
'books' => $this->normalizer->normalize($object->getBooks(), $format, $context)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}