Add own sys_category to record with extbase - typo3

I made the table of one of my models categorizable which works fine if I set categories in the Backend. If I try to add categories via a frontend form I always get the error:
Call to a member function attach() on null
and have no clue why that is so. Maybe anyone of you can help.
In controller I try to add as usual, find the category
$videoCat = $this->categoryRepository->findByUid(28);
and add it like that
$this->video->addTxVideoCat($videoCat);
That's where the error occurs.
Find below how I added the category to the model.
ext_tables.sql `tx_video_cat int(11) DEFAULT '0' NOT NULL,`
extended tca in TCA/Overrides/sys_template.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::makeCategorizable(
'addvideo',
'tx_addvideo_domain_model_video',
// Do not use the default field name ("categories") for pages, tt_content, sys_file_metadata, which is already used
'tx_video_cat',
array(
// Set a custom label
'label' => 'LLL:EXT:addvideo/Resources/Private/Language/locallang.xlf:video_categories',
// This field should not be an exclude-field
'exclude' => FALSE,
// Override generic configuration, e.g. sort by title rather than by sorting
// string (keyword), see TCA reference for details
'l10n_mode' => 'exclude',
// list of keywords, see TCA reference for details
'l10n_display' => 'hideDiff',
)
);
Created extended category repository
namespace Pixelink\Addvideo\Domain\Repository;
class CategoryRepository extends \TYPO3\CMS\Extbase\Domain\Repository\CategoryRepository
Created extended model
namespace Pixelink\Addvideo\Domain\Model;
class Category extends \TYPO3\CMS\Extbase\Domain\Model\Category
{}
Category mapping in typoscript
plugin.tx_addvideo{
persistence {
classes{
Pixelink\Addvideo\Domain\Model\Category {
mapping {
tableName = sys_category
columns {
}
}
}
}
}
}
and in Model\Video.php I added the following
/**
* txVideoCat
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelink\Addvideo\Domain\Model\Category>
*/
protected $txVideoCat = null;
/**
* Get categories
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Pixelink\Addvideo\Domain\Model\Category>
*/
public function getTxVideoCat()
{
return $this->txVideoCat;
}
/**
* Set categories
*
* #param \TYPO3\CMS\Extbase\Persistence\ObjectStorage $txVideoCat
*/
public function setTxVideoCat($txVideoCat)
{
$this->txVideoCat = $txVideoCat;
}
/**
* Add category to a post
*
* #param \Pixelink\Addvideo\Domain\Model\Category $txVideoCat
*/
public function addTxVideoCat(\Pixelink\Addvideo\Domain\Model\Category $txVideoCat)
{
$this->txVideoCat->attach($txVideoCat);
}

You should initialize your property in your Video model constructor:
public function __construct()
{
$this->txVideoCat = new ObjectStorage();
}

Your $this->txVideoCat is null. Use initiailizeObject() method to assign it:
public function initializeObject()
{
$this->txVideoCat = new ObjectStorage();
}

Related

eloquent go to the wrong table just on store method

i have a problem that make my head blow about eloquent went to the wrong table here my code for my activity model
class Activity extends Model{
use HasFactory;
protected $table = 'activities';
protected $guarded = ['id'];
public function getRouteKeyName()
{
return 'slug';
}
and the problem is when i store to the database, eloquent went to the wrong table
here is my store method
public function store(StoreActivityRequest $request)
{
$validatedData = $request->validate([
'name' => 'required',
'slug' => 'required|unique:activies'
]);
Activity::create($validatedData);
return redirect('/activities')->with('success', 'Tindakan Berhasil Ditambahkan');
}
this is my customrequest (actually my laravel make it by default)
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreActivityRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
//
];
}
}
here is the message
Illuminate\Database\QueryException
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'klinik_app.activies' doesn't exist (SQL: select count(*) as aggregate from `activies` where `slug` = konsultasi)
its go to activies table... if i change the tables on migrate file to activies, then the other method gone wrong because it's can't found activities. and this problem just happen to my store method, another method just do the right thing... is there any body can help me?
Update: Added from comments:
public function store(StoreActivityRequest $request) {
$validatedData = $request->validate([
'name' => 'required',
'slug' => 'required|unique:activies'
]);
Activity::create($validatedData);
return redirect('/activities')->with('success', 'Tindakan Berhasil Ditambahkan');
}

How to extend tx:cart with a new field in TYPO3?

I would like to extend the extension cart with a new field to put in the IBAN in the checkout.
So I created a new extension and added a database field with the following code in ext_tables.sql
#
# Table structure for table 'tx_cart_domain_model_order_item'
#
CREATE TABLE tx_cart_domain_model_order_item (
iban varchar(255) DEFAULT '' NOT NULL
);
Now I need to extend the class Item in
ext/cart/Classes/Domain/Model/Order/item.php
I tried to create a file in my extension
ext/cartextend/Classes/Domain/Model/Order/item.php
and tried to extend the class with:
namespace Extcode\Cart\Domain\Model\Order;
use Extcode\Cart\Property\Exception\ResetPropertyException;
class Item extends \Extcode\Cart\Domain\Model\Order
{
/**
* Iban
*
* #var string
*/
protected $iban;
/**
* #return string
*/
public function getIban()
{
return $this->iban;
}
/**
* #param string $iban
*/
public function setIban($iban)
{
$this->iban = $iban;
}
}
I also added an input field that is implemented correctly.
But the IBAN is not saved at all - i guess the extending of the class is wrong.
I really appreciate any hint.
Many thanks! Urs
Maybe you have to extend item.php like this (the rest looks fine):
namespace Extcode\YourExtension\Domain\Model\Order;
class Item extends \Extcode\Cart\Domain\Model\Order\Item
and do not forget to make it known to extbase for you to use iban in the front-end trough typoscript: (I have it to extend cart_products, you'll have to adept it)
config.tx_extbase {
persistence {
classes {
Extcode\CartExtendedProduct\Domain\Model\Product\Product {
mapping {
tableName = tx_cartproducts_domain_model_product_product
recordType =
}
}
}
}
objects {
Extcode\CartProducts\Domain\Model\Product\Product.className = Extcode\CartExtendedProduct\Domain\Model\Product\Product
}
}

Dynamic recipient in form

I have a custom extension with list and detail of teachers. On the detail page I include a form with code:
<formvh:render persistenceIdentifier="1:/form_definitions/myform.yaml" />
I need to set the recipient with the teacher's email shown on the page. How can I do?
You can achieve that by writing a custom form finisher.
Add a hidden field to your form which holds the the ID of the teacher
Fetch that id in your form finisher and load the Teacher model by your repository
A (not complete) example of a form finisher, which loads recipient data from a custom model and sends the mail to this specific data:
class EmailToContactPersonFinisher extends EmailFinisher
{
/**
* Executes this finisher
* #see AbstractFinisher::execute()
*
* #throws FinisherException
*/
protected function executeInternal()
{
/** #var FormRuntime $formRuntime */
$formRuntime = $this->finisherContext->getFormRuntime();
if ($formRuntime->getResponse()->getRequest()) {
if ($formRuntime->getResponse()->getRequest()->hasArgument('contactPerson')) {
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
/** #var ContactPersonRepository $repository */
$contactPersonRepository = $objectManager->get(ContactPersonRepository::class);
/** #var ContactPerson $contactPerson */
$contactPerson = $contactPersonRepository->findByUid($formRuntime->getResponse()->getRequest()->getArgument('contactPerson'));
}
}
// override contactPerson related options
if ($contactPerson instanceof ContactPerson) {
if ($contactPerson->getEmail()) {
$recipientAddress = $contactPerson->getEmail();
}
}
$this->setOption('recipientAddress', $recipientAddress);
parent::executeInternal();
}
}
You can also have a look at the standard emailFinisher, which gives you quick idea on the architecture.
sysext/form/Classes/Domain/Finishers/EmailFinisher.php

How to fix a problem with a nested onetomany relationship not showing in Apigility route

I have a problem using Apigility and Doctrine at their latest versions.
I have 2 entities: Advert and Application on a one to many bidirectional relationship. Many applications can be associated with an Advert and an Advert has many applications.
My tables are created as such (foreign key advert_id on applications table).
I cannot manage to call an advert route with its applications, the applications field is always empty.
After some research ive come to the conclusion that apigility just doesnt know how to render the nested collection. Ive read about hydrators and a package called api-skeletons/zf-doctrine-hydrator that would provide a helper for these doctrine nested relationships, but sadly its not compatible with version 4 of phpro/zf-doctrine-hydration-module and I cannot downgrade because of other packages and dependencies.
<?php
namespace Fil\V1\Entity;
use Doctrine\ORM\Mapping as ORM;
class Applications
{
/**
* #var \Fil\V1\Entity\Adverts
*
* #ORM\ManyToOne(targetEntity="Fil\V1\Entity\Adverts",inversedBy="applications")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="author_id", referencedColumnName="id", nullable=true)
* })
*/
private $advert;
/**
* Set advert.
*
* #param \Fil\V1\Entity\Adverts|null $advert
*
* #return Applications
*/
public function setAdvert(\Fil\V1\Entity\Adverts $advert = null)
{
$this->advert = $advert;
return $this;
}
/**
* Get advert.
*
* #return \Fil\V1\Entity\Adverts|null
*/
public function getAdvert()
{
return $this->advert;
}
}
class Adverts
{
...
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\OneToMany(targetEntity="Fil\V1\Entity\Applications", mappedBy="advert")
*/
private $applications;
/**
* Constructor
*/
public function __construct()
{
$this->applications = new \Doctrine\Common\Collections\ArrayCollection;
}
/**
* Add application.
*
* #param \Fil\V1\Entity\Applications $application
*
* #return Adverts
*/
public function addApplication(\Fil\V1\Entity\Applications $application)
{
$this->applications[] = $application;
return $this;
}
/**
* Remove application.
*
* #param \Fil\V1\Entity\Applications $application
*
* #return boolean TRUE if this collection contained the specified element, FALSE otherwise.
*/
public function removeApplication(\Fil\V1\Entity\Applications $application)
{
return $this->applications->removeElement($application);
}
/**
* Get applications.
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getApplications()
{
return ($this->applications);
}
}
When I call the route for Applications (example: applications/1) it works perfectly, I get my json objet with the Advert relationship correct, with all the information. So the relationship is well conceived.
{
id: 2,
name: "application1",
_embedded:
{
advert:
{
id: 1,
name: "Advert1",
applications: { },
_links:
{
self:
{
href: "http://localhost:8080/adverts/1"
}
}
}
},
_links:
{
self:
{
href: "http://localhost:8080/applications/2"
}
}
}
The problem is when I call and Advert route (example: advert/2). I get the Json with the correct advert information, but the applications field is empty, just {}. There are multiple applications linked to the advert but it just doesnt work.
{
id: 2,
name: "Advert1,
applications: { },
_links:
{
self:
{
href: "http://localhost:8080/adverts/2"
}
}
}
Normally the applications field should be filled with different objets representing each of the applications.
Is there anyway to overcome this issue? thank you!
The ApiSkeletons vendor package has this by design. I'm with you, would prefer it return collections, but the fix is easy: create a new strategy!
Create a strategy class and extend the AllowRemoveByValue strategy of Doctrine.
Overwrite the extract function to return a Collection, either filled or empty
That's it.
Full class:
namespace Application\Strategy;
use DoctrineModule\Stdlib\Hydrator\Strategy\AllowRemoveByValue;
use ZF\Hal\Collection;
class UniDirectionalToManyStrategy extends AllowRemoveByValue
{
public function extract($value)
{
return new Collection($value ?: []);
}
}
Apply this strategy where you need it. E.g. your Advert as many Applications, so the config should be modified like so:
'doctrine-hydrator' => [
'Fil\\V1\\Entity\\ApplicationHydrator' => [
// other config
'strategies' => [
'adverts' => \Application\Strategy\UniDirectionalToManyStrategy::class,
],
],
],
Now collections should be returned.
Quick note: this will only work as a strategy for the *ToMany side.

[Symfony][Form] Add validator/constraint to property only if it has changed

I've got the following scenario: I'm validating appointments and there's a custom validator, which tells the user if his choosen date is valid or not. It's not valid, if the date is already blocked by another entity. This works flawlessly on adding new entities.
Now I'd like to trigger the date validation on edit only if the date itself has changed. So just changing the title of the appointment should not validate the date.
My entity class:
use Doctrine\ORM\Mapping as ORM;
use Acme\Bundle\Validator\Constraints as AcmeAssert;
/**
* Appointment
*
* #ORM\Entity
* #AcmeAssert\DateIsValid
*/
class Appointment
{
/**
* #ORM\Column(name="title", type="string", length=255)
*
* #var string
*/
protected $title;
/**
* #ORM\Column(name="date", type="date")
*
* #var \DateTime
*/
protected $date;
}
The validator class (used as a service):
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the date of an appointment.
*/
class DateIsValidValidator extends ConstraintValidator
{
/**
* {#inheritdoc}
*/
public function validate($appointment, Constraint $constraint)
{
if (null === $date = $appointment->getDate()) {
return;
}
/* Do some magic to validate date */
if (!$valid) {
$this->context->addViolationAt('date', $constraint->message);
}
}
}
The corresponding Constraint class is set to target the entity class.
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class DateIsValid extends Constraint
{
public $message = 'The date is not valid!';
/**
* {#inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
/**
* {#inheritdoc}
*/
public function validatedBy()
{
return 'acme.validator.appointment.date';
}
}
Now I don't find a clean way to depend on a date change. I could simply track the old date in my entity, but that doesn't feel like a proper solution, if I'd like to implement more complex constraints. :[
Cheers
Since symfony 2.3 you can use Form Events to solve this problem. I added the change-check code to my FormType, by storing (and cloning) the original entity at the form creation.
Then added a POST_SUBMIT event listener to check if the fields were changed. The listener can add validation errors to your fields.
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormError;
use Acme\Bundle\Entity\Appointment;
class AppointmentType extends AbstractType
{
private $originalAppointment;
public function __construct(Appointment $original)
{
// save the original entity
$this->originalAppointment = clone $original;
}
// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
// define your fields
$builder->addEventListener(FormEvents::POST_SUBMIT, [$this, 'dateCheckListener']);
}
public function dateCheckListener(FormEvent $event)
{
$appointment = $event->getData();
$form = $event->getForm();
// if no appointments exist, we can skip the check
if (empty($appointment) || empty($this->originalAppointment)) {
return;
}
if ($appointment->getDate() !== $this->originalAppointment->getDate()) {
// the dates changed, you can call your validator here
if ('dates are not valid') {
$form->get('date')->addError(new FormError('We have a problem.'));
}
}
}
}
In your controller, you can create this formType with the original appointment:
$appointment = $this->getYourAppointmentSomehow();
$form = $this->createForm(new AppointmentType($appointment), $appointment);
Maybe you will find this article useful, to check which property is changed. Everything is possible in symfony. You might end up writing entity listeners, listener resolvers and so on. Things can get ultra advanced.
http://docs.doctrine-project.org/en/latest/reference/change-tracking-policies.html
Pay attention to the setter method:
public function setData($data)
{
if ($data != $this->data) {
$this->_onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
Do you see the trick?:)
I would also use !== operator to also check variable type.
You can also simplify things. You dont need to call _onPropertyChanged, but call the function, which will set a property 'dateChanged' to true. Then use method:
public function getGroupSequence()
{
if($this->dateChanged)
{
return ['date_check'];
}
else
{
return false;
}
}
And also tell your class that it implements GroupSequenceProviderInterface.
You can then use the validation group in your validation.yml for example.
maybe you want to try it with a preUpdate-Listener instead of a custom validation constraint?
Section 10.5.4 in the doctrine documentation gives an example of a validation listener "ValidCreditCardListener".
i know this will not work for automagic form validation, but i think it's the fastest way atm.
edit:
another option could be to use #UniqueEntiy constraint for the date field of your Appointment class. this will not break form validation but will cause an additional database query (as far as i know)