Laravel: ID as ValueObject is null after save - laravel

I'm using ValueObject casting as an ID of my model. Everything works fine when I get a record from database, however when it coming to saving, the ID is null. If I comment "casts" out, ID is correct.
Example:
$game = new Game($data);
$game->created_by = $userId; // Id ValueObject
$game->save();
dd($game);
// attributes:
// "id" => null,
// "created_by" => Id{#value: 10},
Id ValueObject:
class Id
{
public function get($model, $key, $value, $attributes)
{
$this->value = $value;
return $this;
}
public function set($model, $key, $value, $attributes)
{
$this->value = $value;
}
public function value(): int
{
return $this->value;
}
public function __toString()
{
return (string) $this->value;
}
}
Model:
class Game extends Model
{
protected $casts = [
'id' => Id::class
];
}
What can I do with it?
Thanks in advance

Okey, I think that there should be a ValueObject and a CastingObject, I ended up with something similar to this:
class Game extends Model
{
protected $casts = [
'id' => IdCast::class
];
}
class IdCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return new Id(
$attributes['id']
);
}
public function set($model, $key, $value, $attributes)
{
return [
'id' => $value
];
}
}
class Id
{
private $value;
public function __construct($id)
{
$this->value = $id;
}
public function value(): int
{
return $this->value;
}
public function __toString()
{
return (string) $this->value;
}
}

Related

Files collection with Api-Platform and VichUploadBundle

I am using Api-Platform in latest version and I would like my user to be able to attach multiple files when writing a note.
As a first step, I'm trying to make this work with Postman. I chose VichUploadBundle as the documentation says.
Only I'm blocked for publishing a note with attachments. I think I did it right overall, but I have to intervene again on one point, I can't find the subtlety.
I specify that I am able to create a simple file, without relation. Thanks to the custom controller. But for the relationship, I obviously need the MultipartDecoder.
The File Object :
#[Vich\Uploadable]
#[ORM\Entity]
#[ApiResource(
types: ['https://schema.org/MediaObject'],
operations: [
new Get(),
new GetCollection(),
new Post(
controller: CreateMediaObjectAction::class,
openapiContext: [
'requestBody' => [
'content' => [
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'properties' => [
'file' => [
'type' => 'string',
'format' => 'binary'
]
]
]
]
]
]
],
deserialize: false
)
],
normalizationContext: ['groups' => ['media_object:get']]
)]
class MediaObject
{
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
private ?int $id = null;
#[ApiProperty(types: ['https://schema.org/contentUrl'])]
#[Groups([
'media_object:get'
])]
public ?string $contentUrl = null;
#[Vich\UploadableField(mapping: "media_object", fileNameProperty: "filePath")]
public ?File $file = null;
#[ORM\Column(nullable: true)]
public ?string $filePath = null;
#[ORM\ManyToOne(inversedBy: 'files')]
#[ORM\JoinColumn(nullable: false)]
private Note $note;
public function getId(): ?int
{
return $this->id;
}
/**
* #return Note
*/
public function getNote(): Note
{
return $this->note;
}
/**
* #param Note $note
*/
public function setNote(Note $note): void
{
$this->note = $note;
}
}
Note Object :
#[ORM\Entity(repositoryClass: NoteRepository::class)]
#[ApiResource(
operations: [
new GetCollection(
normalizationContext: ['groups' => ['note:get:collection']],
security: "is_granted('ROLE_ADMIN') or is_granted('ROLE_SIEGE')"
),
new Post(
inputFormats: ['multipart' => ['multipart/form-data']],
denormalizationContext: ['groups' => ['note:post:item']],
securityPostDenormalize: "is_granted('POST_NOTE', object)"
),
new Get(
security: "is_granted('GET_NOTE', object)"
),
new Put(
denormalizationContext: ['groups' => ['note:post:item']],
security: "is_granted('PUT_NOTE', object)"
),
new Delete(
security: "is_granted('DELETE_NOTE', object)"
)
],
normalizationContext: ['groups' => ['note:get:item']],
paginationClientItemsPerPage: true,
)]
class Note
{
...
#[Groups([
'note:get:item',
'note:post:item'
])]
#[ORM\OneToMany(mappedBy: 'note', targetEntity: MediaObject::class)]
public Collection $files;
public function __construct()
{
$this->files = new ArrayCollection();
}
...
}
MultipartDecoder :
final class MultipartDecoder implements DecoderInterface
{
public const FORMAT = 'multipart';
public function __construct(private RequestStack $requestStack) {}
/**
* {#inheritdoc}
*/
public function decode(string $data, string $format, array $context = []): ?array
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
return null;
}
return array_map(static function (string $element) {
// Multipart form values will be encoded in JSON.
$decoded = json_decode($element, true);
return \is_array($decoded) ? $decoded : $element;
}, $request->request->all()) + $request->files->all();
}
/**
* {#inheritdoc}
*/
public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
}
Denormalizer :
final class UploadedFileDenormalizer implements DenormalizerInterface
{
/**
* {#inheritdoc}
*/
public function denormalize($data, string $type, string $format = null, array $context = []): UploadedFile
{
return $data;
}
/**
* {#inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null): bool
{
return $data instanceof UploadedFile;
}
}
Normalizer :
final class MediaObjectNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private const ALREADY_CALLED = 'MEDIA_OBJECT_NORMALIZER_ALREADY_CALLED';
public function __construct(private StorageInterface $storage)
{
}
public function normalize($object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
$context[self::ALREADY_CALLED] = true;
$object->contentUrl = $this->storage->resolveUri($object, 'file');
return $this->normalizer->normalize($object, $format, $context);
}
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{
if (isset($context[self::ALREADY_CALLED])) {
return false;
}
return $data instanceof MediaObject;
}
}
The message changes depending on whether or not there are brackets on the key files
files[] :
"Expected IRI or nested document for attribute "files", "object" given."

Problem for working with DTO - Laravel, PHP

For example I have code like this:
$this->users = $data['data'];
$this->month = $data['month'];
$this->year = $data['year'];
But I need to use DTO. For example I used this function in DTO class:
public function getUsers(): string
{
return $this->users;
}
And as I understand I need to add it to the first code. But I don't understand how to use DTO for my the first code. Can you explain me please?
upd
Now I have:
public function __construct($data, $jobWatcherId)
{
$this->jobWatcherId = $jobWatcherId;
$jobsDTO = new JobsDTO($data['data'], $data['month'], $data['year'],
$data['working_days'], $data['holiday_hours'],
$data['advance_payroll_date'], $data['main_payroll_date']);
}
public function handle()
{
$jobWatcher = JobWatcher::find($this->jobWatcherId);
try {
$startedAt = now();
$jobWatcher->update([
'status_id' => JobWatcherStatusEnum::PROCESSING,
'started_at' => $startedAt,
]);
$redmineService = new RedmineAPIService();
foreach ($jobsDTO->getUsers() as $user) {
}
And for line foreach ($jobsDTO->getUsers() as $user) I have Undefined variable '$jobsDTO'
Your question is a bit unclear, but as I understand it, you want to instantiate a DTO with the above data?
You could have a class like:
class UsersDTO
{
public array $users;
public int $month;
public int $year;
public function __construct(array $users, int $month, int $year)
{
$this->users = $users;
$this->month = $month;
$this->year = $year;
}
public function getUsers(): array
{
return $this->users;
}
public function getMonth(): int
{
return $this->month;
}
public function getYear(): int
{
return $this->year;
}
}
and then somewhere else call:
$usersDTO = new UsersDTO($data['data'], $data['month'], $data['year']);
// Do something with $usersDTO->getUsers();

Can I use a belongsTo method with name 'asset'?

I have a model "SalesContract" which has a "belongsTo" relationship with a class called "Asset". However, it does not work (I cannot set or get).
Could it be an issue with the "asset()" helper method?
If I change the name of my method to something like "related_asset()", then it works.
This does NOT work:
public function asset()
{
return $this->belongsTo(Asset::class);
}
This DOES work:
public function related_asset()
{
return $this->belongsTo(Asset::class);
}
Full model:
class SalesContract extends Model
{
use SoftDeletes;
use Commentable;
const icon_class = 'far fa-file-signature';
const default_buyer_fee = 100;
const default_carproof_fee = 36.45;
protected $fillable = [
'number', 'asset_id', 'seller_id', 'buyer_id', 'buyer_representative', 'sale_date', 'sale_price',
'apply_sales_taxes_to_sale_price', 'buyer_fee', 'carproof_fee', 'deposit'
];
protected $casts = [
'sale_date' => 'datetime',
'sale_price' => 'float',
'carproof_fee' => 'float',
'buyer_fee' => 'float',
'deposit' => 'float',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime'
];
protected $appends = [
'subtotal', 'taxable_amount', 'sales_taxes', 'total', 'balance'
];
protected static function boot()
{
parent::boot();
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('created_at', 'desc');
});
static::saving(function($table) {
if (empty($table->id)) {
if ($current_user = Auth::user()) {
$table->created_by_user_id = $current_user->id;
}
}
});
}
public function __construct(array $attributes = [])
{
if (empty($this->sale_date)) {
$this->sale_date = Carbon::today()->format('Y-m-d');
}
if (empty($this->id)) {
if (empty($this->number)) {
if ($asset = $this->asset) {
$this->number = $asset->external_file_number ?? $asset->internal_file_number;
}
}
$this->buyer_fee = $this->buyer_fee ?? self::default_buyer_fee;
$this->carproof_fee = $this->carproof_fee ?? self::default_carproof_fee;
$this->apply_sales_taxes_to_sale_price = $this->apply_sales_taxes_to_sale_price ?? 1;
}
parent::__construct($attributes);
}
public function __toString()
{
return __('sales_contracts.item_label', ['number' => $this->number ?? $this->id]);
}
public function scopeFilter($query, $filters)
{
$filters = is_array($filters) ? array_filter($filters) : [];
return $query->where($filters);
}
public function asset()
{
return $this->belongsTo(Asset::class);
}
public function seller()
{
return $this->belongsTo(Contact::class);
}
public function buyer()
{
return $this->belongsTo(Contact::class);
}
public function created_by_user()
{
return $this->belongsTo(User::class);
}
public function getSubtotalAttribute()
{
return $this->sale_price + $this->carproof_fee + $this->buyer_fee;
}
public function getTaxableAmountAttribute()
{
if ($this->apply_sales_taxes_to_sale_price) {
return $this->subtotal;
} else {
return $this->subtotal - $this->sale_price;
}
}
public function getSalesTaxesAttribute()
{
$sales_taxes = [];
if ($seller = $this->seller) {
foreach ($seller->sales_tax_numbers as $tax_number) {
if ($tax_number->use) {
if ($sales_tax = $tax_number->sales_tax) {
$sales_taxes[] = [
'sales_tax' => $sales_tax,
'name' => $sales_tax->name,
'rate' => $sales_tax->rate,
'label' => $sales_tax->label,
'number' => $tax_number->number,
'amount' => round($this->taxable_amount * $sales_tax->rate, 2)
];
}
}
}
}
return $sales_taxes;
}
public function getSalesTaxesTotalAttribute()
{
$total = 0;
foreach ($this->sales_taxes as $sales_tax) {
$total += $sales_tax['amount'];
}
return $total;
}
public function getTotalAttribute()
{
return $this->subtotal + $this->sales_taxes_total;
}
public function getBalanceAttribute()
{
return $this->total - $this->deposit;
}
}
From controller:
$sales_contract = new SalesContract;
if ($request->has('sales_contract')) {
$sales_contract->fill($request->input('sales_contract'));
}
Result of dd($request->input()):
array:1 [▼
"sales_contract" => array:1 [▼
"asset_id" => "11754"
]
]
(Yes, Asset with ID 11754 does exist.)
by default Name of relation is depended on 'foreign_key'
if you want to set different name of relation than foreign key just provide foreign key and other_key along with relation declaration
public function asset()
{
return $this->belongsTo(Asset::class,related_asset,id);
}
Problem solved.
I had to remove the following code from my __construct() method as it was breaking the relationship somehow:
if (empty($this->number)) {
if ($asset = $this->asset) {
$this->number = $asset->external_file_number ?? $asset->internal_file_number;
}
}

Inserting Data in Pivot Table

The two tables tbl_product_manager and tbl_tags with many to many relations. I used eloquent to to make a relations between the corresponding models. I am able to to insert the data in these two table but the problem is the pivot table is not updated correspondingly.
Controller.php:
public function addProduct()
{
$rules = array('product_name' => 'required',
'product_url' => 'required');
$validator = Validator::make(Input::all(), $rules);
if($validator->fails()){
Session::flash('class', 'alert alert-error');
Session::flash('message', 'Some fields are missing');
return View::make('admin.product.add');
}
else {
$productName = Input::get('product_name');
$productUrl = Input::get('product_url');
$productUrl = preg_replace('/[^A-Za-z0-9\-]/', '', $productUrl);
$productExist = ProductManagementModel::checkExist($productUrl);
if( count($productExist)!=0) {
$message = 'product <b>'.$productName.'</b> with url <b>'.$productUrl.'</b> is already exist';
Session::flash('class', 'alert alert-error');
Session::flash('message', $message);
return View::make('admin.product.add');
}
else {
$imageFile = Input::file('userfile');
$destinationPath = 'uploads/products/';
$rEFileTypes = "/^\.(jpg|jpeg|gif|png){1}$/i";
$maximum_filesize = 1 * 1024 * 1024;
if($imageFile) {
$filename = $imageFile->getClientOriginalName();
$extension = strrchr($filename, '.');
$size = $imageFile->getSize();
$new_image_name = "products" . "_" . time();
if ($size <= $maximum_filesize && preg_match($rEFileTypes, $extension)) {
$attachment = $imageFile->move($destinationPath, $new_image_name.$extension);
} else if (preg_match($rEFileTypes, $extension) == false) {
Session::flash('class', 'alert alert-error');
Session::flash('message', 'Warning : Invalid Image File!');
return View::make('admin.product_management.add');
} else if ($size > $maximum_filesize) {
Session::flash('class', 'alert alert-error');
Session::flash('message', "Warning : The size of the image shouldn't be more than 1MB!");
return View::make('admin.product_management.add');
}
}
$logo = isset($attachment) ? $new_image_name . $extension : NULL;
$objectProduct = new ProductManagementModel;
$objectProduct->product_name = Input::get('product_name');
$objectProduct->product_url = $productUrl;
$objectProduct->category_id = Input::get('category_id');
$objectProduct->product_cost = Input::get('product_cost');
$objectProduct->product_short_description = Input::get('product_short_description');
$objectProduct->product_description = Input::get('product_description');
$objectProduct->is_active = Input::get('is_active');
$objectProduct->created_at = Auth::user()->id;
$objectProduct->updated_at = Auth::user()->id;
if($logo != '')
{
$objectProduct->product_attachment = $logo;
}
$objectTags = new TagModel;
$objectTags->size_id = Input::get('size_id');
$objectTags->brand_id = Input::get('brand_id');
$objectTags->color_id = Input::get('color_id');
$objectTags->food_id = Input::get('food_id');
$objectTags->save();
//$tag = new TagModel::all();
$objectProduct->save();
if(isset($request->tags)) {
$post->Tags()->sync($request->tags, false);
}
if($objectProduct->id) {
Session::flash('class', 'alert alert-success');
Session::flash('message', 'Product successfully added');
return View::make('admin.product_management.add');
} else {
Session::flash('class', 'alert alert-error');
Session::flash('message', 'Something error');
return View::make('admin.product_management.add');
}
}
}
}
ProductManagementModel.php
<?php
use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;
class ProductManagementModel extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
protected $table = 'product_manager';
public function Tags(){
return $this->belongsToMany('TagModel', 'product_tag', 'product_id', 'tag_id');
}
public function Categories(){
return $this->hasOne('CategoriesModel', 'id');
}
public static function getAllProducts(){
return $products = ProductManagementModel::with('categories','tags')->get();
}
public static function checkExist($url)
{
return $products = DB::table('product_manager')
->where('is_deleted', 0)
->where('product_url', $url)
->first();
}
}
TagModel.php
<?php
use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;
class TagModel extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
protected $table = 'tag';
public function ProductManagents() {
return $this->belongsToMany('ProductManagentModel');
}
public function Color(){
return $this->hasOne('ColorModel', 'color_id');
}
public function Brand() {
return $this->hasOne('BrandproModel','brand_id');
}
public function size() {
return $this->hasOne('SizeModel','size_id');
}
public function food() {
return $this->hasOne('FoodModel','food_id');
}
}
During my research i found that using sync function will be appropriate to updated the pivot table. But I failed to use it.
I am expecting to resolve this problem or something new way to find out the solution.
Thanks in advance.
Look at attach, detach or synch method :
https://laravel.com/docs/5.5/eloquent-relationships#updating-many-to-many-relationships
Note it's more easily if you respect the eloquent naming convention
http://www.rappasoft.com/articles/laravel-eloquent-naming-convention-guide/

Laravel - Prevent from create empty key in database

I have a table where I keep all the settings that will be used on the website, they are saved in the cache, I'm trying to upload the favicon, however when uploading the image the favicon row is updated and an empty key value with the temp path is created at the same time, how can I solve this?
You can see the empty field in the image...
Route
Route::put('/', ['as' => 'setting.update', 'uses' => 'Admin\AdminConfiguracoesController#update']);
Model
class Setting extends Model
{
protected $table = 'settings';
public $timestamps = false;
protected $fillable = ['value'];
}
Controller
class AdminConfiguracoesController extends AdminBaseController
{
private $repository;
public function __construct(SettingRepository $repository){
parent::__construct();
$this->repository = $repository;
}
public function update(Request $request, Factory $cache)
{
$settings = $request->except('_method', '_token');
$this->repository->update($settings);
$cache->forget('settings');
return redirect()->back();
}
}
Repository
class SettingRepository{
private $settings;
public function __construct(Setting $settings)
{
$this->settings = $settings;
}
public function update($key, $value = null)
{
if (is_array($key))
{
foreach ($key as $name => $value)
{
if( $name == "website_favicon" ){
$imageName = $key['website_favicon']->getClientOriginalName();
$this->update($name, asset('public/images/website/'.$imageName));
$key['website_favicon']->move(
base_path() . '/public/images/website/', $imageName
);
} else{
$this->update($name, $value);
}
}
}
$setting = $this->settings->firstOrCreate(['name' => $key]);
$setting->value = $value;
$setting->save();
}
public function lists()
{
return $this->settings->lists('value', 'name')->all();
}
}
The problem is a missing return statement after the foreach loop in your repository. The code after the loop will be executed. $key is an array and $value is the temp value of the uploaded file, which will be set inside the loop.
As I mentioned in my comment, you shouldn't use the repository to upload files. Do it in your controller instead:
AdminConfiguracoesController.php
class AdminConfiguracoesController extends AdminBaseController
{
private $repository;
public function __construct(SettingRepository $repository)
{
parent::__construct();
$this->repository = $repository;
}
public function update(Request $request, Factory $cache)
{
$settings = $request->except('_method', '_token', 'website_favicon');
if ($request->hasFile('website_favicon'))
{
$this->uploadImage($request->file('website_favicon'), 'website_favicon');
$cache->forget('website_favicon');
}
$this->repository->update($settings);
$cache->forget('settings');
return redirect()->back();
}
private function uploadImage(UploadedFile $image, $key)
{
$image->move(public_path('images/website'), $image->getClientOriginalName());
$this->repository->update($key, $image->getClientOriginalName());
}
}
SettingRepository.php
class SettingRepository
{
private $settings;
public function __construct(Setting $settings)
{
$this->settings = $settings;
}
public function update($key, $value = null)
{
if (is_array($key))
{
foreach ($key as $name => $value)
{
$this->update($name, $value);
}
return; // This was missing!
}
$setting = $this->settings->firstOrCreate(['name' => $key]);
$setting->value = $value;
$setting->save();
}
public function lists()
{
return $this->settings->lists('value', 'name')->all();
}
}
You can refactor this even further to use a Job that uploads the image, but this would be overkill for now.

Resources