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."
Related
I want to insert some data for the first time the module is being installed.
This is my folder structure
Navien/Custom
-- Setup/InstallData.php
-- Model/
-- StateData.php
-- StateMaster.php
-- ResourceModel/
-- StateMaster.php
-- StateMaster/
-- Collection.php
Here is content of my script InstallData.php
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Eav\Model\Config;
use Navien\Custom\Model\StateData;
class InstallData implements InstallDataInterface
{
private $eavSetupFactory;
private $eavConfig;
private $stateDataMaster;
public function __construct(EavSetupFactory $eavSetupFactory, Config $eavConfig, StateData $stateData)
{
$this->eavSetupFactory = $eavSetupFactory;
$this->eavConfig = $eavConfig;
$this->stateDataMaster = $stateData;
}
public function install(
ModuleDataSetupInterface $setup,
ModuleContextInterface $context
)
{
$data = [
['code' => 'AL', 'name' => 'Alabama', 'abbreviation' => 'Ala.', 'country' => 'US'],
['code' => 'AK', 'name' => 'Alaska', 'abbreviation' => 'Alaska', 'country' => 'US']
];
$this->stateDataMaster->insertStates($data);
}
}
Model/StateData.php
namespace Navien\Custom\Model;
use Navien\Custom\Model\ResourceModel\StateMaster;
use Psr\Log\LoggerInterface;
use Magento\Framework\View\Element\Template\Context;
// class StateData extends \Magento\Framework\View\Element\Template
class StateData
{
protected $_logger;
protected $_stateMaster;
protected $_objectManager;
public function __construct(
LoggerInterface $logger,
StateMaster $stateMaster,
Context $context,
array $data = []
)
{
// parent::__construct($context, $data);
$this->_objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$this->_logger = $logger;
}
public function insertStates($states)
{
if( !empty( $states ) )
{
$stateMasterRepository = $this->_objectManager->get('Navien\Custom\Model\StateMaster');
$stateData = $stateMasterRepository->getCollection()->getData();
foreach( $states as $key => $state )
{
if( (array_search($state['code'], array_column($stateData, 'code')) === FALSE) )
{
$stateMaster = $this->_objectManager->get('Navien\Custom\Model\StateMaster');
$stateMaster->setData( $state );
$stateMaster->save();
}
}
}
}
public function execute()
{
}
}
Model/StateMaster.php
namespace Navien\Custom\Model;
use \Magento\Framework\Model\AbstractModel;
use \Psr\Log\LoggerInterface;
class StateMaster extends AbstractModel
{
protected $_logger;
public function __construct(
LoggerInterface $logger
)
{
$this->_init('Navien\Custom\Model\ResourceModel\StateMaster');
}
}
Model/ResourceModel/StateMaster.php
namespace Navien\Custom\Model\ResourceModel;
use \Psr\Log\LoggerInterface;
class StateMaster extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
protected function _construct()
{
$this->_init('navien_states','ID');
}
}
Navien\Custom\Model\ResourceModel\StateMaster
namespace Navien\Custom\Model\ResourceModel\StateMaster;
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
public function __construct(
\Magento\Framework\Data\Collection\EntityFactory $entityFactory,
\Psr\Log\LoggerInterface $logger,
\Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory $optionValueCollectionFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
\Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
) {
$this->_optionValueCollectionFactory = $optionValueCollectionFactory;
$this->_storeManager = $storeManager;
parent::__construct(
$entityFactory,
$logger,
$fetchStrategy,
$eventManager,
$connection,
$resource
);
$this->logger = $logger;
}
protected function _construct()
{
$this->_init('Navien\Custom\Model\StateMaster','Navien\Custom\Model\ResourceModel\StateMaster');
}
}
Now I am getting this error
Error: Call to a member function dispatch() on null in /var/www/dev/vendor/magento/framework/Model/AbstractModel.php:701
Stack trace: #0 /var/www/dev/vendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php(412): Magento\Framework\Model\AbstractModel->beforeSave()
#1 /var/www/dev/vendor/magento/framework/Model/AbstractModel.php(655): Magento\Framework\Model\ResourceModel\Db\AbstractDb->save()
#2 /var/www/dev/app/code/Navien/Custom/Model/StateData.php(62): Magento\Framework\Model\AbstractModel->save()
It's because your constructor does not match the parent block constructor.
Model/StateMaster.php
namespace Navien\Custom\Model;
use \Magento\Framework\Model\AbstractModel;
class StateMaster extends AbstractModel
{
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = []
) {
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
protected function _construct()
{
$this->_init(\Navien\Custom\Model\ResourceModel\StateMaster::class);
}
}
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;
}
}
I have this code to udpdate max length of an attribute
class UpgradeData implements UpgradeDataInterface
{
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
if ($context->getVersion()
&& version_compare($context->getVersion(), '1.0.2') < 0
) {
$table = $setup->getTable('eav_attribute');
$setup->getConnection()
->update($table, ['frontend_class' => 'validate-length maximum-length-70'], 'attribute_id = 73');
}
$setup->endSetup();
}
}
It works fine bu instad of attribute_id = 73 I want to put attribute_code= name but it does not work.
Try this
class UpgradeData implements UpgradeDataInterface
{
/**
* #var \Magento\Eav\Setup\EavSetupFactory
*/
private $eavSetupFactory;
public function __construct(\Magento\Eav\Setup\EavSetupFactory $eavSetupFactory)
{
$this->eavSetupFactory = $eavSetupFactory;
}
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
if ($context->getVersion()
&& version_compare($context->getVersion(), '1.0.2') < 0
) {
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$this->eavSetup->updateAttribute(
'catalog_product',
'ATTRIBUTE_CODE_GOES_HERE',
'frontend_class',
'validate-length maximum-length-70'
);
}
$setup->endSetup();
}
}
Watch out for typos. I didn't check the code.
i want to ask about error "Too few arguments to function Illuminate\Database\Eloquent\Model::setAttribute(), 1 passed "
this is my code :
Dataanggota.php
class Dataanggota extends Model
{
protected $table;
protected $primaryKey = "";
protected $casts = ['cno' => 'string'];
protected $guarded = [ ];
public $incrementing = false;
public $timestamps = false;
public $rules = array();
public $nicename = array();
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = config("consyst.dataanggota.table");
$this->primaryKey=config('consyst.dataanggota.primary_key');
$this->rules = array(
);
}
DataAnggotaControllers.php
public function insertBank()
{
// dd($this->request->cno);
if ($this->request->ajax()) {
try {
$metas = new Hubbank;
$metas->cno = $this->request->cno;
$metas->no_urut = $this->request->no_urut;
$metas->kdcab = $this->request->kdcab;
$metas->nm_bank = $this->request->nm_bank;
$metas->alternate = $this->request->alternate;
$metas->dari_tahun = $this->request->dari_tahun;
$metas->jns_produk = $this->request->jns_produk;
$metas->save();
return \Response::json(['status' => 99, 'msg' => 'Data has been Added']);
} catch (\Exeception $e) {
return \Response::json(['status' => 0, 'msg' => 'Error Accoured!']);
}
} else {
return \Response::view('errors.401');
}
}
This is error massage
FatalThrowableError in Model.php line 2870:
Type error: Too few arguments to function Illuminate\Database\Eloquent\Model::setAttribute(), 1 passed in
C:\xampp\htdocs\project\consyst\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php on line 2878 and exactly 2 expected
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;
}
}