I'm working with a legacy big ball of mud that uses a latin1 database but works with utf8 strings. Each time the application reads or writes to database, it decodes or encodes by hand and stores utf8 encoded strings in the latin1 database.
When writing, it does something like:
$value = utf8_encode("Iñtërnâtiônàlizætiøn")
mysql_query("INSERT INTO table (key) VALUES ($value)")
So the stored value is Iñtërnâtiônà lizætiøn
And when reading:
$result = mysql_query("SELECT key FROM table")
$value = utf8_decode($result) // Wich results on "Iñtërnâtiônàlizætiøn" again
How can I manage the same database using Doctrine 2 and respecting that strange behaviour?
The following code will work as expected when used in my Entity, but I'm looking for a cleaner and DRY solution.
public function setKey($value)
{
$this->key = utf8_encode($value);
}
public function getKey()
{
return utf8_decode($this->key);
}
You could create your own custom "string" type and override it's class in Doctrine\DBAL\Types\Type. Type::convertToDatabaseValue and Type::convertToPHPValue would be what you want to override.
<?php
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class Utf8StringType extends StringType
{
/**
* {#inheritdoc}
*/
public function convertToDatabaseValue($value, AbstractPlatform $p)
{
// convert from utf8 to latin1
return mb_convert_encoding($value, 'ISO-8859-1', 'UTF-8');
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $p)
{
// convert from latin1 to utf8
return mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1');
}
}
Then put the type into Doctrine with a new name or replace the string type:
<?php
\Doctrine\DBAL\Types\Type::addType('utf8string', 'Utf8StringType');
// replace the default string type
\Doctrine\DBAL\Types\Type::overrideType('string', 'Utf8StringType');
Related
I've been looking for a solution for a while but none of the one I find really allows me to do what I want. I would just like to create routes that don't necessarily require an entity or id to be used. Can you help me the documentation is not clear to do this.
Thank you beforehand.
As you can read in the General Design Considerations, just make an ordinary PHP class (POPO). Give it an ApiResource annontation like this:
* #ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={}
* )
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
You should add your path if your class is not in the Entity folder.
Api Platform will expect json to be posted and try to unserialize it into an instance of your class. Make a custom DataPersister to process the instance, for example if your class is App\ApiCommand\Doit:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\ApiCommand\Doit;
use App\ApiResult\DoitResult;
final class DoitDataPersister implements ContextAwareDataPersisterInterface
{
public function supports($data, array $context = []): bool
{
return $data instanceof Doit;
}
public function persist($data, array $context = [])
{
// code to process $data
$result = new DoitResult();
$result->description = 'Hello world';
return $result;
}
public function remove($data, array $context = [])
{
// will not be called if you have no delete operation
}
}
If you need Doctrine, add:
public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}
See Injecting Extensions for how to use it.
Notice that the result returned by ::persist is not an instance of Doit. If you return a Doit api platform will try to serialize that as the result of your operation. But we have marked Doit as an ApiResource so (?) api platform looks for an item operation that can retrieve it, resulting in an error "No item route associated with the type App\ApiCommand\Doit". To avoid this you can return any object that Symfonies serializer can serialize that is not an ApiResource. In the example an instance of DoitResult. Alternatively you can return an instance of Symfony\Component\HttpFoundation\Response but then you have to take care of the serialization yourself.
The post operation should already work, but the swagger docs are made from metadata. To tell api platform that it should expect a DoitResult to be returned, change the #ApiResource annotation:
* collectionOperations={
* "post"={
* "output"=DoitResult::class
* }
* },
This will the add a new type for DoitResult to the swagger docs, but the descriptions are still wrong. You can correct them using a SwaggerDecorator. Here is one for a 201 post response:
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'short explanation about DoitResult';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/doit']['post']['responses']['201']['description'] = 'Additional explanation about DoitResult';
$responseContent = $docs['paths']['/doit']['post']['responses']['201']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
If your post operation is not actually creating something you may not like the 201 response. You can change that by specifying the response code in the #ApiResource annotation, for example:
* collectionOperations={
* "post"={
* "output"=DoitResult::class,
* "status"=200
* }
* },
You may want to adapt the SwaggerDecorator accordingly.
Creating a "get" collection operation is similar, but you need to make a DataProvider instead of a DataPersister. The chapter9-api branch of my tutorial contains an example of a SwaggerDecorator for a collection response.
Thanks you for answer. I had some information but not everything. I will try the weekend.
Laravel 5.4 supports the Postgres TIMESTAMP WITH TIME ZONE field type in migrations:
$table->timestampTz('scheduled_for');
Laravel can be set up to convert date fields (DATE, DATETIME, TIMESTAMP) into Carbon objects (and does so by default for the created_at and updated_at TIMESTAMP fields), but putting scheduled_for into the $dates field causes an error with the timezone-aware version:
InvalidArgumentException with message 'Trailing data'
Looking in the database and tinker, the field's value appears to be something like 2017-06-19 19:19:19-04. Is there a native way to get a Carbon object out of one of these field types? Or am I stuck using an accessor?
Resurrecting this question, hopefully with a helpful answer that gets accepted.
Laravel assumes a Y-m-d H:i:s database timestamp format. If you're using a Postgres timestampz column, that's obviously different. You need to tell Eloquent how to get Carbon to parse that format.
Simply define the $dateFormat property on your model like so:
Class MyModel extends Eloquent {
protected $dateFormat = 'Y-m-d H:i:sO';
}
Credit where credit is due: I found this solution in a GitHub issue
Put this inside your model
protected $casts = [
'scheduled_for' => 'datetime' // date | datetime | timestamp
];
Using $dates is more likely obsolete as $casts do the same stuff (maybe except $dateFormat attribute which can work only for $dates fields iirc, but I saw some complaining on it)
Edit
I was testing Carbon once on Laravel 5.4 and I created a trait for it
this is not production level code yet so include it in your model on your own risk
<?php namespace App\Traits;
use Carbon\Carbon;
trait castTrait
{
protected function castAttribute($key, $value)
{
$database_format = 'Y-m-d H:i:se'; // Store this somewhere in config files
$output_format_date = 'd/m/Y'; // Store this somewhere in config files
$output_format_datetime = 'd/m/Y H:i:s'; // Store this somewhere in config files
if (is_null($value)) {
return $value;
}
switch ($this->getCastType($key)) {
case 'int':
case 'integer':
return (int) $value;
case 'real':
case 'float':
case 'double':
return (float) $value;
case 'string':
return (string) $value;
case 'bool':
case 'boolean':
return (bool) $value;
case 'object':
return $this->fromJson($value, true);
case 'array':
case 'json':
return $this->fromJson($value);
case 'collection':
return new BaseCollection($this->fromJson($value));
case 'date':
Carbon::setToStringFormat($output_format_date);
$date = (string)$this->asDate($value);
Carbon::resetToStringFormat(); // Just for sure
return $date;
case 'datetime':
Carbon::setToStringFormat($output_format_datetime);
$datetime = (string)$this->asDateTime($value);
Carbon::resetToStringFormat();
return $datetime;
case 'timestamp':
return $this->asTimestamp($value);
default:
return $value;
}
}
/**
* Return a timestamp as DateTime object with time set to 00:00:00.
*
* #param mixed $value
* #return \Carbon\Carbon
*/
protected function asDate($value)
{
return $this->asDateTime($value)->startOfDay();
}
/**
* Return a timestamp as DateTime object.
*
* #param mixed $value
* #return \Carbon\Carbon
*/
protected function asDateTime($value)
{
$carbon = null;
$database_format = [ // This variable should also be in config file
'datetime' => 'Y-m-d H:i:se', // e -timezone
'date' => 'Y-m-d'
];
if(empty($value)) {
return null;
}
// If this value is already a Carbon instance, we shall just return it as is.
// This prevents us having to re-instantiate a Carbon instance when we know
// it already is one, which wouldn't be fulfilled by the DateTime check.
if ($value instanceof Carbon) {
$carbon = $value;
}
// If the value is already a DateTime instance, we will just skip the rest of
// these checks since they will be a waste of time, and hinder performance
// when checking the field. We will just return the DateTime right away.
if ($value instanceof DateTimeInterface) {
$carbon = new Carbon(
$value->format($database_format['datetime'], $value->getTimezone())
);
}
// If this value is an integer, we will assume it is a UNIX timestamp's value
// and format a Carbon object from this timestamp. This allows flexibility
// when defining your date fields as they might be UNIX timestamps here.
if (is_numeric($value)) {
$carbon = Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that format. Again, this provides for simple date
// fields on the database, while still supporting Carbonized conversion.
if ($this->isStandardDateFormat($value)) {
$carbon = Carbon::createFromFormat($database_format['date'], $value)->startOfDay();
}
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
$carbon = Carbon::createFromFormat(
$database_format['datetime'], $value
);
return $carbon;
}
}
Currently, when I convert a model to JSON, all Carbon date fields are casted like so:
"end_time": {
"date": "2017-02-03 23:59:00.000000",
"timezone_type": 3,
"timezone": "Europe/London"
}
I want it casted using the Atom notation.
This can be done in carbon like so:
$order->end_time->toAtomString()
where $date is a Carbon date.
How can I make a model convert dates in the atom format when converting it to JSON?
I am aware that it is possible to append data like so: https://laravel.com/docs/5.3/eloquent-serialization#appending-values-to-json
But this does not change the format of an existing value?
With the risk of reviving a zombie, I shall present an alternate solution to this problem:
Override the serializeDate method defined by the trait HasAttributes:
/**
* Prepare a date for array / JSON serialization.
*
* #param \DateTimeInterface $date
* #return string
*/
protected function serializeDate(DateTimeInterface $date)
{
return $date->toAtomString();
}
The link you provided should be the solution for you as long as you are willing to use a different name than "end_time". You could append "end_time_formatted", or something similar.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
protected $appends = ['end_time_formatted'];
public function getEndTimeFormattedAttribute()
{
return $this->end_time->toAtomString();
}
}
Then any time you cast a model to json, it will include "end_time_formatted" with it.
Another option for you (if you require keeping the same name) would be to override the toJson method by copying it into your model. I'd probably advise against this, but it would prevent the need to say $this->created_at = $this->created_at->toAtomString() each time before you cast it to JSON.
/**
* Convert the model instance to JSON.
*
* #param int $options
* #return string
*
* #throws \Illuminate\Database\Eloquent\JsonEncodingException
*/
public function toJson($options = 0)
{
$atom = $this->created_at->toAtomString();
$json = json_encode($this->jsonSerialize(), $options);
if (JSON_ERROR_NONE !== json_last_error()) {
throw JsonEncodingException::forModel($this, json_last_error_msg());
}
$json = json_decode($json);
$json->created_at = $atom;
$json = json_encode($json);
return $json;
}
I wasn't able to get this to work by changing the value at the top of the method, so I was forced to json_decode, and then re-encode, which doesn't feel great to me. If you do use this route I'd suggest digging a little deeper to try get it working without the need to decode.
An alternative to use is to format your date which you receive from model
You could use a helper method which converts your date object to the format you wish using carbon.
Carbon facilitates formatting abilities and I feel this is what you are looking for :
your_function_name($created_at_date, $format = "jS M Y")
{
$carbon = new \Carbon\Carbon($created_at_date);
$formatted_date = $carbon->format($format);
return $formatted_date;
}
Hope this helps.
I would like to create a UUID/GUID for a specific column in Laravel 5. So i am using the Library from Webpatser to get an UUID in Laravel5.
I would like to set this as default for a specific column, so i guess i should use attributes, or?
But when i use
protected $attributes = array('guid' => Uuid::generate(4)->string);
ill always get an error with:
syntax error, unexpected '(', expecting ')'
I am not sure, because the syntax looks fine and when ill try
protected $attributes = array('guid' => 'dsadasfasfsaf');
Everything works fine (guid is a varchar(36) field) - and
Uuid::generate(4)->string
returns a string.
Do i need anything else to create a default value for my model? Thanks in advance.
PHP can't parse non-trivial expressions in initializers.
You can do this:
class YourClass
{
protected $attributes;
function __construct()
{
$this->attributes = array('guid' => Uuid::generate(4)->string);
}
}
Or this by using the setter method:
class YourClass
{
protected $attributes;
public function setAttributes($attributes)
{
$this->attributes = $attributes;
}
}
$classInstance = new YourClass;
$classInstance->setAttributes(array('guid' => Uuid::generate(4)->string));
I'm having problems using find*() magic methods of Doctrine2 when the field has an underscore in between.
$repository->findByName("Hello"); // Works
$repository->findByIsEnabled(true);
Entity 'Acme\SecurityBundle\Entity\Package' has no field 'isEnabled'.
You can therefore not call 'findByIsEnabled' on the entities'
repository.
This is the simple entity definition in YAML for replicating the error:
Acme\SecurityBundle\Entity\Package:
type: entity
repositoryClass: Acme\SecurityBundle\Repository\PackageRepository
table: security_package
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 255
unique: true
is_enabled:
type: boolean
I recall having had the same problem and think I solved it by writing something like this :
$repository->findBy(array('is_enabled' => true));
Let's look at the code :
<?php
/**
* Adds support for magic finders.
*
* #return array|object The found entity/entities.
* #throws BadMethodCallException If the method called is an invalid find* method
* or no find* method at all and therefore an invalid
* method call.
*/
public function __call($method, $arguments)
{
if (substr($method, 0, 6) == 'findBy') {
$by = substr($method, 6, strlen($method));
$method = 'findBy';
} else if (substr($method, 0, 9) == 'findOneBy') {
$by = substr($method, 9, strlen($method));
$method = 'findOneBy';
} else {
throw new \BadMethodCallException(
"Undefined method '$method'. The method name must start with ".
"either findBy or findOneBy!"
);
}
if ( !isset($arguments[0])) {
// we dont even want to allow null at this point, because we cannot (yet) transform it into IS NULL.
throw ORMException::findByRequiresParameter($method.$by);
}
$fieldName = lcfirst(\Doctrine\Common\Util\Inflector::classify($by));
if ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName)) {
return $this->$method(array($fieldName => $arguments[0]));
} else {
throw ORMException::invalidFindByCall($this->_entityName, $fieldName, $method.$by);
}
}
The key line is here:
$fieldName = lcfirst(\Doctrine\Common\Util\Inflector::classify($by));
Now let's have a look to classify :
<?php
/**
* Convert a word in to the format for a Doctrine class name. Converts 'table_name' to 'TableName'
*
* #param string $word Word to classify
* #return string $word Classified word
*/
public static function classify($word)
{
return str_replace(" ", "", ucwords(strtr($word, "_-", " ")));
}
It looks like you're supposed to write your fields "likeThis" if you want this to work.