Background:
I have build my web application using CodeIgniter because it was the only framework I could grasp easily enough to get going quickly. Now seeing the unbelievably advanced functionality of symfony and the PSR standards I am hyped to get into it all.
Dialemma
I am not sure how to approach the model layer with symfony/doctrine. As I understand it: doctrine generates an entity class for a database table like so...
This class contains a bunch of setter/getter functions.
My mental block at the moment is that I don't understand how I am supposed to add to functionality to my model layer.
To understand where I am coming from take a look at a typical CodeIgniter Model that I am currently working with. This one handles discount coupons.
<?php
/**
* This class handles all coupon codes
*/
class Coupon_Model extends CI_Model
{
/**
* gets a specific coupon
* #param string $coupon_code
* #return obj
*/
public function getCoupon($coupon_code)
{
$this->db->where('coupon_code', $coupon_code);
$query = $this->db->get('coupons');
return $query->row();
}
/**
* gets all coupons associated with a course
* #param int $course_id
* #return array
*/
public function getCourseCoupons($course_id)
{
$this->db->where('course_id', $course_id);
$query = $this->db->get('coupons');
return $query->result();
}
/**
* generates a string of 10 random alphanumeric numbers
* #return string
*/
public function generateCouponCode()
{
return strtoupper(substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 10));
}
/**
* creates a new active coupon
* #param array $data
* #param string $coupon_code
* #return bool
*/
public function createCoupon($data, $coupon_code = null)
{
if ($coupon_code !== '') {
$data['coupon_code'] = $coupon_code;
} else {
$data['coupon_code'] = $this->generateCouponCode();
}
return $this->db->insert('coupons', $data);
}
/**
* checks if a coupon is valid
* #param string $coupon_code
* #param int $course_id
* #return bool
*/
public function checkCoupon($coupon_code, $course_id = null)
{
$this->db->where('coupon_code', $coupon_code);
$query = $this->db->get('coupons');
$coupon = $query->row();
// if coupon code exists
if ($coupon === null) {
return false;
}
// if coupon is for the right course
if ($coupon->course_id !== $course_id && $course_id !== null) {
return false;
}
// if coupon code has not expired
if ($coupon->expiry_date <= $this->Time_Model->getCarbonNow()->timestamp) {
return false;
}
return true;
}
/**
* deletes a coupon record
* #param int coupon_id
* #return bool
*/
public function deleteCoupon($coupon_id)
{
$this->db->where('coupon_id', $coupon_id);
return $this->db->delete('coupons');
}
/**
* applys the coupon discount
* #param int $price
* #param float $discount (percentage)
*/
public function applyDiscount($price, $discount)
{
$price = $price - (($discount / 100) * $price);
return $price;
}
}
As you can see it is pretty straight forward, if I wanted to add functionality I would literally just create a new function.
To use this model I would simply load it on the Controller like this:
$this->model->load('coupons/Coupon_Model');
$this->Coupon_Model->getCoupon($coupon_code);
Simple, done and dusted... unfortunately I am not sure how to implement this sort of functionality with symfony/doctrine.
Will I need to create a new class separate from the entity and add extra functionality to this class? Or should I add more functions to the entity class?
Take for example my simple function which generates the coupon code:
/**
* generates a string of 10 random alphanumeric numbers
* #return string
*/
public function generateCouponCode()
{
return strtoupper(substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 10));
}
Where would be the best place to put this function? Under AppBundle/models/coupons?
I have clearly picked up bad habits from CodeIgniter and have a feeling that I am approaching this the wrong way.
Symfony + Doctrine ORM comes with a lot of the default needs for the replacement of CodeIgniter models by using the EntityManager within your Controller(s).
For example
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DefaultController extends Controller
{
/**
* #Route("/{id}/show", name="app_show", defaults={"id" = 1})
*/
public function showAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
if (!$coupon = $em->find('AppBundle:Coupon', $id)) {
throw new NotFoundException('Unknown Coupon Specified');
}
//see below to see how this was implemented
$similarCoupons = $em->getRepository('AppBundle:Coupon')
->filterCourse($coupon->course);
return $this->render('AppBundle:template.twig', [
'coupon' => $coupon,
'similarCoupons' => $similarCoupons
]);
}
/**
* #Route("/new", name="app_new")
*/
public function newAction(Request $request)
{
//use Symfony Form Component instead
$em = $this->getDoctrine()->getManager();
$coupon = new \AppBundle\Entity\Coupon;
//calls __construct to call generateCouponCode
$coupon->setName($request->get('name'));
$em->persist($coupon);
$em->flush();
return $this->redirectToRoute('app_show', ['id' => $coupon->getId()]);
}
//...
}
You want to specify the functionality you want each entity to have when working with it from within the Entity class.
That it becomes available without needing to revisit the repository, since an Entity should never be aware of the EntityManager.
In effect, each Entity can be considered their own models.
For example $coupon->generateCouponCode(); or $this->generateCouponCode() from within the entity.
Otherwise you would use a Repository of your Doctrine Database Entity(ies) to add more complex functionality.
// /src/AppBundle/Entity/Coupon.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repository="CouponRepository")
*/
class Coupon
{
/**
* #var integer
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
* #ORM\Column(name="name", type="string", length=50)
*/
private $name;
/**
* #var string
* #ORM\Column(name="coupon_code", type="string", length=10)
*/
private $couponCode;
/**
* #var Course
* #ORM\ManyToOne(targetEntity="Course", inversedBy="coupons")
* #ORM\JoinColumn(name="course", referencedColumnName="id")
*/
private $course;
//...
public function __construct()
{
//optionally create code when persisting a new database entry by using LifeCycleCallbacks or a Listener instead of this line.
$this->couponCode = $this->generateCouponCode();
}
//...
/**
* generates a string of 10 random alphanumeric numbers
* #return string
*/
public function generateCouponCode()
{
return strtoupper(substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, 10));
}
}
Then your custom queries would go into your Repository.
// /src/AppBundle/Entity/CouponRepository.php
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
class CouponRepository extends EntityRepository
{
/**
* filters a collection of Coupons that matches the supplied Course
* #param Course $course
* #return array|Coupons[]
*/
public function filterCourse(Course $course)
{
$qb = $this->createQueryBuilder('c');
$expr = $qb->expr();
$qb->where($expr->eq('c.course', ':course'))
->setParameter('course', $course);
return $qb->getQuery()->getResult();
}
}
Additionally you can filter collections of an association (Foreign Key) reference within your entity.
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
//...
class Course
{
//...
/**
* #var ArrayCollection|Coupon[]
* #ORM\OneToMany(targetEntity="Coupon", mappedBy="course")
*/
private $coupons;
public function __construct()
{
$this->coupons = new ArrayCollection;
}
/**
* #return ArrayCollection|Coupon[]
*/
public function getCoupons()
{
return $this->coupons;
}
/**
* #param string $name
* #return \Doctrine\Common\Collections\Collection|Coupon[]
*/
public function getCouponsByName($name)
{
$criteria = Criteria::create();
$expr = $criteria::expr();
return $this->coupons->matching($criteria->where($expr->eq('name', $name)));
}
}
Related
I'm currently experiencing issues when trying to filter my results when using an external API source (Stripe) in API-Platform.
What I need to be able to do, is return a list of subscriptions for a specified customer. So going to http://localhost/api/subscriptions?customer=123foo would return all records matched to this customer.
Now, the code below is throwing an error because of the ORM\Filter and would be functional without it, as the actual filtering is performed on Stripes API, not by me, BUT, I really want the Swagger-API GUI to have the filter box.
In short, how do I get the Annotations in my Entity to display, searchable fields within the Swagger UI when using an external data source.
What I have is an Entity as below (simplified for example purposes):
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use Symfony\Component\Serializer\Annotation\Groups;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Annotation\ApiFilter;
/**
* Subscriptions allow you to charge a customer on a recurring basis. A subscription ties a customer to a particular plan you've created.
* #ApiResource()
* #ApiFilter(SearchFilter::class, properties={"customer": "exact"})
* #package App\Entity
*/
class Subscription
{
/**
* Unique identifier for the object.
* #ApiProperty(identifier=true)
* #var string | null
*/
protected $id;
/**
* ID of the customer who owns the subscription.
* #var string | null
*/
protected $customer;
// Plus a bunch more properties and their Getters & Setters
}
And the SubscriptionCollectionDataProvider:
<?php
namespace App\DataProvider;
use App\Entity\Subscription;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Controller\BaseController;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Class SubscriptionCollectionDataProvider
* #package App\DataProvider
* #author dhayward
*/
final class SubscriptionCollectionDataProvider extends BaseController implements CollectionDataProviderInterface, RestrictedDataProviderInterface
{
protected $requestStack;
/**
* SubscriptionCollectionDataProvider constructor.
* #param RequestStack $requestStack
*/
public function __construct(RequestStack $requestStack)
{
$this->request = $requestStack->getCurrentRequest();
}
/**
* #param string $resourceClass
* #param string|null $operationName
* #param array $context
* #return bool
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Subscription::class === $resourceClass;
}
/**
* #param string $resourceClass
* #param string|null $operationName
* #return \Generator
* #throws \Stripe\Error\Api
*/
public function getCollection(string $resourceClass, string $operationName = null): \Generator
{
$customer = $this->request->get("customer");
$data = \Stripe\Subscription::all(["customer" => $customer]);
foreach($data['data'] as $subscriptionObject){
$this->serializer()->deserialize(json_encode($subscriptionObject), Subscription::class, 'json', array('object_to_populate' => $subscription = new Subscription()));
yield $subscription;
}
}
}
Error result, which is presumably because I'm using an ORM/Filter without any ORM setup:
Call to a member function getClassMetadata() on null
Any pointers would be greatly appreciated.
So I finally managed to work it out. It was as simple as creating my own version of the SearchFilter, implementing ApiPlatform\Core\Api\FilterInterface.
<?php
namespace App\Filter;
use ApiPlatform\Core\Api\FilterInterface;
/**
* Class SearchFilter
* #package App\Filter
*/
class SearchFilter implements FilterInterface
{
/**
* #var string Exact matching
*/
const STRATEGY_EXACT = 'exact';
/**
* #var string The value must be contained in the field
*/
const STRATEGY_PARTIAL = 'partial';
/**
* #var string Finds fields that are starting with the value
*/
const STRATEGY_START = 'start';
/**
* #var string Finds fields that are ending with the value
*/
const STRATEGY_END = 'end';
/**
* #var string Finds fields that are starting with the word
*/
const STRATEGY_WORD_START = 'word_start';
protected $properties;
/**
* SearchFilter constructor.
* #param array|null $properties
*/
public function __construct(array $properties = null)
{
$this->properties = $properties;
}
/**
* {#inheritdoc}
*/
public function getDescription(string $resourceClass): array
{
$description = [];
$properties = $this->properties;
foreach ($properties as $property => $strategy) {
$filterParameterNames = [
$property,
$property.'[]',
];
foreach ($filterParameterNames as $filterParameterName) {
$description[$filterParameterName] = [
'property' => $property,
'type' => 'string',
'required' => false,
'strategy' => self::STRATEGY_EXACT,
'is_collection' => '[]' === substr($filterParameterName, -2),
];
}
}
return $description;
}
}
i have two entities Survey.php and Choice.php I create a form to add new survey and multi choices, I used a many-to-one relation between the two entities, the problem is when I submit for a new survey, the foreign key of choice entity return null
here's my code
Survey.PHP
/**
* Survey
*
* #ORM\Table(name="survey")
* #ORM\Entity(repositoryClass="AppBundle\Repository\SurveyRepository")
*/
class Survey
{
/....
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Choice", mappedBy="survey_id",cascade="persist")
* #ORM\JoinColumn(nullable=false, referencedColumnName="id")
*/
private $choice;
public function __construct()
{
$this->choice = new ArrayCollection();
}
/**
* Add choice
*
* #param \AppBundle\Entity\Choice $choice
*
* #return Survey
*/
public function addChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice[] = $choice;
$choice->setSurvey($this);
return $this;
}
/**
* Remove choice
*
* #param \AppBundle\Entity\Choice $choice
*/
public function removeChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice->removeElement($choice);
}
/**
* Get choice
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChoice()
{
return $this->choice;
}
}
Choice.php
/**
* Choice
* #ORM\Table(name="choice")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ChoiceRepository")
*/
class Choice
{
/**
* #var int
* #ORM\ManyToOne(targetEntity="Survey",inversedBy="choice")
*/
private $survey;
/**
* Set survey
*
* #param \AppBundle\Entity\Survey $survey
*
* #return Choice
*/
public function setSurveyId(\AppBundle\Entity\Survey $survey)
{
$this->survey = $survey;
return $this;
}
/**
* Get surveyId
*
* #return \AppBundle\Entity\Survey
*/
public function getSurveyId()
{
return $this->survey_id;
}
}
SurveyController.php
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Survey;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* Survey controller.
*
*/
class SurveyController extends Controller
{
/**
* Creates a new survey entity.
* #param Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request)
{
$survey = new Survey();
//
$form = $this->createForm('AppBundle\Form\SurveyType', $survey);
$form->handleRequest($request);
$survey->setCreateDate(new \DateTime('NOW'));
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($survey);
$em->flush();
return $this->redirectToRoute('survey_show', ['id' => $survey->getId()]);
}
return $this->render('survey/new.html.twig', [
'survey' => $survey,
'form' => $form->createView(),
]);
}
any suggestion, btw I think the problem is in the getters and setters )
Link the choice to the survey:
// Survey
public function addChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice[] = $choice;
$choice->setSurvey($this);
return $this;
}
And change the survey_id stuff to survey. Dealing with objects not ids. And of course Survey::choice should be Survey::choices. The name changes might seem minor but will make your easier to maintain.
I fixed the problem by adding a for each loop inside the SurveyController.php
and it works just fine
SurveyController.php
if ($form->isSubmitted() && $form->isValid())
{
foreach ($survey->getChoices() as $choice){
$choice->setSurvey($survey);
}
$em = $this->getDoctrine()->getManager();
$em->persist($survey);
$em->flush();
not "THE best solution" but it gets the job done
It worked with me, but I had to remove the explicit foreign key mapping with the "inversedBy" setting from the class definition. I use a composite foreign key (using two columns), which maybe makes things harder though...
I am using Nicolas Widart's Laravel Modules package to help manage a large app, and keep everything separated into logical modules. I would like to be able to drop in different modules and have them play nicely without any extra configuration.
All of my modules will define interfaces and default implementations that allow the application (the system controlling which modules are loaded) to specify that it wants to use a specific implementation instead, through dependency injection.
I am able to make some assumptions by having some modules require others, for example a payment processing module (Module PP) can assume that a payment is tied to a user (with which the interface for a user is defined in another module, Module U).
My ideal scenario is that I could add to an existing PHP interface that is defined in another required module. For example, being able to retrieve a user from a repository defined in Module U and call a method on it that was defined in Module PP.
Once Module PP resolves the interface (again, through dependency injection) from Module U to a class, I want my method from Module PP to be callable on that class.
I have been able to achieve this using the __call magic method as below.
Extensions Module
This module defines the core operations to add to an existing interface.
IsExtendable Interface
<?php
namespace Modules\Extensions\Contracts;
interface IsExtendable
{
/**
* Get the list of extensions for this entity.
*
* #return array
*/
public static function getExtensions();
/**
* Adds an extension to this entity.
*
* #param string $name
* #param mixed $function
*/
public static function addExtension($name, $function);
/**
* Checks whether the entity has the given extension.
*
* #param string $name
*
* #return bool
*/
public static function hasExtension($name);
/**
* Call the extension if it exists, or pass it further up the chain.
*
* #param string $name
* #param mixed $arguments
*
* #return mixed
*/
public function __call($name, $arguments);
}
IsExtendable Trait
<?php
namespace Modules\Extensions;
trait IsExtendable
{
/** #var $extensions */
private static $extensions = [];
/**
* Get the list of extensions for this entity.
*
* #return array
*/
public static function getExtensions()
{
return self::$extensions;
}
/**
* Adds an extension to this entity.
*
* #param string $name
* #param mixed $function
*/
public static function addExtension($name, $function)
{
if(is_callable($function) == FALSE)
{
throw new \InvalidArgumentException('Function must be callable.');
}
self::$extensions[$name] = $function;
}
/**
* Checks whether the entity has the given extension.
*
* #param string $name
*
* #return bool
*/
public static function hasExtension($name)
{
return array_key_exists($name, self::getExtensions()) == TRUE;
}
/**
* Calls the extension if it exists, or passes it further up the chain.
*
* #param string $name
* #param mixed $arguments
*
* #return mixed
*/
public function __call($name, $arguments)
{
if(self::hasExtension($name) == TRUE)
{
$callable = self::getExtensions()[$name];
return call_user_func_array($callable, array_merge(array($this), $arguments));
}
else
{
return parent::__call($name, $arguments);
}
}
}
Service Provider
<?php
namespace Modules\Extensions\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
class ExtensionServiceProvider extends ServiceProvider
{
/**
* #param string $implementation
* #param string $functionName
*
* #return callable
*/
public function prepareExtension($implementation, $functionName)
{
return $implementation . '::' . $functionName;
}
/**
* #param string $contract
* #param string $implementation
*
* #return void
*/
public function extractExtensions($contract, $implementation)
{
$reflection = new \ReflectionClass($implementation);
$methods = [];
foreach($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method)
{
// TODO: May be able to use $method->getClosure() here
// https://stackoverflow.com/questions/8299886/php-get-static-methods
$methods[] = $method->getName();
}
$this->registerExtensions($contract, $methods, $implementation);
}
/**
* #param string $contract
* #param string $name
* #param string $function
*
* #return void
*/
public function registerExtension($contract, $name, $function)
{
// Resolve the contract to an implementation
$base = app($contract);
// Check that it is suitable for extension
if($base instanceof IsExtendableContract)
{
$base::addExtension($name, $function);
}
}
/**
* #param string $contract
* #param array $extensions
* #param string|null $implementation
*
* #return void
*/
public function registerExtensions($contract, array $extensions = [], $implementation = NULL)
{
// Resolve the contract to an implementation
$base = app($contract);
// Check that it is suitable for extension
if($base instanceof IsExtendableContract)
{
foreach($extensions as $name => $function)
{
if(is_int($name) == TRUE)
{
if(is_string($function) == TRUE)
{
$name = $function;
}
else
{
throw new \InvalidArgumentException('All extensions must have a valid name.');
}
}
if(is_string($function) == TRUE)
{
if(strpos($function, '::') === FALSE && $implementation != NULL)
{
$function = $this->prepareExtension($implementation, $function);
}
}
$base::addExtension($name, $function);
}
}
}
}
Module U
User Interface
<?php
namespace Modules\Auth\Contracts\Entities;
interface User
{
/**
* #return int
*/
public function getId();
/**
* #return string
*/
public function getName();
/**
* #return string
*/
public function getEmail();
/**
* #return \DateTime
*/
public function getCreatedAt();
/**
* #return \DateTime
*/
public function getUpdatedAt();
}
User Implementation
<?php
namespace Modules\Auth\Entities;
use Modules\Extensions\Contracts\IsExtendable as IsExtendableContract;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Extensions\IsExtendable;
class User implements
IsExtendableContract,
UserContract
{
use IsExtendable;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return string
*/
public function getEmail()
{
return $this->email;
}
/**
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->created_at;
}
/**
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updated_at;
}
}
Module PP
User Extension
<?php
namespace Modules\Test\Entities\Extensions;
use Modules\Auth\Contracts\Entities\User;
class UserExtension
{
/**
* #param User $context
*/
public static function getCardLastFour($context)
{
return $context->card_last_four;
}
/**
* #param User $context
*/
public static function getCardBrand($context)
{
return $context->card_brand;
}
/**
* #param User $context
*/
public static function getStripeId($context)
{
return $context->stripe_id;
}
}
Service Provider
<?php
namespace Modules\Test\Providers\Extensions;
use Modules\Auth\Contracts\Entities\User as UserContract;
use Modules\Test\Entities\Extensions\UserExtension;
use Modules\Extensions\Providers\ExtensionServiceProvider;
class StripeExtensionProvider extends ExtensionServiceProvider
{
public function boot()
{
// TODO: Set the contract as a static field on the extension to then automatically extract from all extension files in a folder
$this->extractExtensions(UserContract::class, UserExtension::class);
}
}
My question is, is this method scalable (across maybe 10 modules), and can you foresee any issues with it? Or is there a better/more popular (and supported) way to do this? I don't want to get 2 years into a project and discover that I really hate the way I've implemented this.
I know that this concept won't support IDE autocompletion out of the box but I could build in a way to generate the PHPDocs similar to this package.
I have researched the Decorator pattern but this feels clunky in that I would always need to rely on a new implementation within each module, instead of just adding to the existing one.
I realise this is a big question so my sincere thanks to anyone willing to have a look at it!
Check out Laravel's macroable trait. It's basically the same idea, and Laravel uses it all over the place.
So yes, it scales - up to a certain point. Like almost everything else, this is a tool that can be abused. Use it with a dash of common sense, and you should be OK.
I try to build a custom registration form which should be displayed in a custom block and I don't want to insert the normal registration form and alter it via a hook or use an extension like form_block, because I want to learn the ways how drupal 8 works with forms.
My block looks like this:
<?php
namespace Drupal\ajax_registration_form\Plugin\Block;
use Drupal\ajax_registration_form\Form\AjaxRegistrationForm;
use Drupal\Core\Block\BlockBase;
/**
* Provides a 'AjaxRegistrationBlock' block.
*
* #Block(
* id = "ajax_registration_block",
* admin_label = #Translation("Ajax registration block"),
* )
*/
class AjaxRegistrationBlock extends BlockBase {
/**
* {#inheritdoc}
*/
public function build() {
$content = \Drupal::formBuilder()->getForm(AjaxRegistrationForm::class);
return $content;
}
}
My custom registration form looks like this:
<?php
namespace Drupal\ajax_registration_form\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\RegisterForm;
class AjaxRegistrationForm extends RegisterForm {
/**
* {#inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
return parent::form($form, $form_state);
}
}
I just tried to extend the normal RegisterForm and in the first step I just wanted to return the parent form to see if it works. But it doesn't...
Error message:
Fatal error: Call to a member function getEntityTypeId() on null in /Users/*******/Sites/priv/radweiser/web/core/lib/Drupal/Core/Entity/EntityForm.php on line 77
I think it's the missing user entity in the form, but I don't know how I can "put" this entity in my form.
I found the solution in the code of the formblock module.
I altered my block to something like this:
<?php
namespace Drupal\ajax_registration_form\Plugin\Block;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Block\Annotation\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a 'AjaxRegistrationBlock' block.
*
* #Block(
* id = "ajax_registration_block",
* admin_label = #Translation("Ajax registration block"),
* )
*/
class AjaxRegistrationBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The entity manager
*
* #var \Drupal\Core\Entity\EntityManagerInterface.
*/
protected $entityManager;
/**
* The entity form builder
*
* #var \Drupal\Core\Entity\EntityManagerInterface.
*/
protected $entityFormBuilder;
/**
* Constructs a new UserRegisterBlock plugin
*
* #param array $configuration
* A configuration array containing information about the plugin instance.
* #param string $plugin_id
* The plugin_id for the plugin instance.
* #param mixed $plugin_definition
* The plugin implementation definition.
* #param \Drupal\Core\Entity\EntityManagerInterface $entityManager
* The entity manager.
* #param \Drupal\Core\Entity\EntityFormBuilderInterface $entityFormBuilder
* The entity form builder.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entityManager, EntityFormBuilderInterface $entityFormBuilder) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityManager = $entityManager;
$this->entityFormBuilder = $entityFormBuilder;
}
/**
* #inheritdoc
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager'),
$container->get('entity.form_builder')
);
}
/**
* Implements \Drupal\block\BlockBase::build().
*/
public function build() {
$build = array();
$account = $this->entityManager->getStorage('user') ->create(array());
$build['form'] = $this->entityFormBuilder->getForm($account, 'register');
$build['form']['account']['mail']['#description'] = t('');
kint($build['form']['account']);
return $build;
}
/**
*Implements \Drupal\block\BlockBase::blockAccess().
*
* #param \Drupal\Core\Session\AccountInterface $account
*
* #return bool|\Drupal\Core\Access\AccessResult
*/
public function blockAccess(AccountInterface $account) {
return ($account->isAnonymous()) && (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY);
}
}
Now I can alter the form and implement ajax logic using the Form Api (alter the mail input description as example)
I have placed this block in front page content but it is not visible
<span data-big-pipe-placeholder-id="callback=Drupal%5Cblock%5CBlockViewBuilder%3A%3AlazyBuilder&args%5B0%5D=userregistrationform&args%5B1%5D=full&args%5B2%5D&token=-MyZMBSv_tseO1_TExVECdGUQnyGrlvE9eST64je7Ho"></span>
it shows like this
FYI I have changed permissions to isAnonymous()
I am not writing "what did I try" or "what is not working" since I can think of many ways to implement something like this. But I cannot believe that no one did something similar before and that is why I would like to ask the question to see what kind of Doctrine2 best practices show up.
What I want is to trigger an event on a property change. So let's say I have an entity with an $active property and I want a EntityBecameActive event to fire for each entity when the property changes from false to true.
Other libraries often have a PropertyChanged event but there is no such thing available in Doctrine2.
So I have some entity like this:
<?php
namespace Application\Entity;
class Entity
{
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var boolean
* #ORM\Column(type="boolean", nullable=false)
*/
protected $active = false;
/**
* Get active.
*
* #return string
*/
public function getActive()
{
return $this->active;
}
/**
* Is active.
*
* #return string
*/
public function isActive()
{
return $this->active;
}
/**
* Set active.
*
* #param bool $active
* #return self
*/
public function setActive($active)
{
$this->active = $active;
return $this;
}
}
Maybe ChangeTracking Policy is what you want, maybe it is not!
The NOTIFY policy is based on the assumption that the entities notify
interested listeners of changes to their properties. For that purpose,
a class that wants to use this policy needs to implement the
NotifyPropertyChanged interface from the Doctrine\Common namespace.
Check full example in link above.
class MyEntity extends DomainObject
{
private $data;
// ... other fields as usual
public function setData($data) {
if ($data != $this->data) { // check: is it actually modified?
$this->onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
UPDATE
This is a full example but silly one so you can work on it as you wish. It just demonstrates how you do it, so don't take it too serious!
entity
namespace Football\TeamBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="country")
*/
class Country extends DomainObject
{
/**
* #var int
*
* #ORM\Id
* #ORM\Column(type="smallint")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
*
* #ORM\Column(type="string", length=2, unique=true)
*/
protected $code;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set code
*
* #param string $code
* #return Country
*/
public function setCode($code)
{
if ($code != $this->code) {
$this->onPropertyChanged('code', $this->code, $code);
$this->code = $code;
}
return $this;
}
/**
* Get code
*
* #return string
*/
public function getCode()
{
return $this->code;
}
}
domainobject
namespace Football\TeamBundle\Entity;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;
abstract class DomainObject implements NotifyPropertyChanged
{
private $listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->listeners[] = $listener;
}
protected function onPropertyChanged($propName, $oldValue, $newValue)
{
$filename = '../src/Football/TeamBundle/Entity/log.txt';
$content = file_get_contents($filename);
if ($this->listeners) {
foreach ($this->listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
file_put_contents($filename, $content . "\n" . time());
}
}
}
}
controller
namespace Football\TeamBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Football\TeamBundle\Entity\Country;
class DefaultController extends Controller
{
public function indexAction()
{
// First run this to create or just manually punt in DB
$this->createAction('AB');
// Run this to update it
$this->updateAction('AB');
return $this->render('FootballTeamBundle:Default:index.html.twig', array('name' => 'inanzzz'));
}
public function createAction($code)
{
$em = $this->getDoctrine()->getManager();
$country = new Country();
$country->setCode($code);
$em->persist($country);
$em->flush();
}
public function updateAction($code)
{
$repo = $this->getDoctrine()->getRepository('FootballTeamBundle:Country');
$country = $repo->findOneBy(array('code' => $code));
$country->setCode('BB');
$em = $this->getDoctrine()->getManager();
$em->flush();
}
}
And have this file with 777 permissions (again, this is test) to it: src/Football/TeamBundle/Entity/log.txt
When you run the code, your log file will have timestamp stored in it, just for demonstration purposes.