An exception occurred while executing 'SELECT NEXTVAL - symfony

I have an error I don't know why when I want to insert data in database? the same code in local machine works fine but in remote host it show this error :
An exception occurred while executing 'SELECT NEXTVAL('"BookContent_id_seq"')':
SQLSTATE[42P01]: Undefined table: 7 ERROR: relation "BookContent_id_seq" does not
exist LINE 1: SELECT NEXTVAL('"BookContent_id_seq"')
for some reason of host server I can't use symfony command in remote server when using FTP
Entity:
<?php
namespace App\Entity;
use App\Repository\BookContentRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=BookContentRepository::class)
* #ORM\Table(name="`BookContent`")
*/
class BookContent
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $page;
/**
* #ORM\Column(type="string", length=255)
*/
private $title;
/**
* #ORM\Column(type="text")
*/
private $description;
public function getId(): ?int
{
return $this->id;
}
public function getPage(): ?string
{
return $this->page;
}
public function setPage(string $page): self
{
$this->page = $page;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
}
Repository
<?php
namespace App\Repository;
use App\Entity\BookContent;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #method BookContent|null find($id, $lockMode = null, $lockVersion = null)
* #method BookContent|null findOneBy(array $criteria, array $orderBy = null)
* #method BookContent[] findAll()
* #method BookContent[] findBy(array $criteria, array $orderBy = null, $limit =
null, $offset = null)
*/
class BookContentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BookContent::class);
}
}
Migration file:
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20221030220934 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE book_content (id INT NOT NULL, page VARCHAR(255) NOT
NULL, title VARCHAR(255) NOT NULL, description TEXT NOT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE book_content');
}
}
Also I use postgreSQL and the table already exist in database with the name book_content but as I said I can't run any command in remote server?

Related

Symfony join query with One-To-Many relation

I am quite new to symfony as in DQL. I have a problem with query, namely I want to compare the ID's between 'term_id' from table 'TermAssign' with 'id' from table Term and then, the one's who are matching are to be rendered on a template. Relation between Term and TermAssign is OneToMany. There is also a table Offer, which has relation OneToMany with table TermAssign.
This is my Term.php:
<?php
namespace App\OfferBundle\Entity;
use App\OfferBundle\Repository\TermRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=TermRepository::class)
* #ORM\Table(name="term")
*/
class Term
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255, nullable=true, name="term_description")
*/
private $term_description;
/**
* #ORM\OneToMany(targetEntity=TermAssign::class, mappedBy="term")
*/
private $assign;
public function __construct()
{
$this->assign = new ArrayCollection();
}
function getId(): ?int
{
return $this->id;
}
public function getTermDescription(): ?string
{
return $this->term_description;
}
public function setTermDescription(?string $term_description): self
{
$this->term_description = $term_description;
return $this;
}
/**
* #return Collection|TermAssign[]
*/
public function getAssign(): Collection
{
return $this->assign;
}
public function addAssign(TermAssign $assign): self
{
if (!$this->assign->contains($assign)) {
$this->assign[] = $assign;
$assign->setTerm($this);
}
return $this;
}
public function removeAssign(TermAssign $assign): self
{
if ($this->assign->removeElement($assign)) {
// set the owning side to null (unless already changed)
if ($assign->getTerm() === $this) {
$assign->setTerm(null);
}
}
return $this;
}
}
This is Offer.php:
<?php
namespace App\OfferBundle\Entity;
use App\OfferBundle\Repository\OfferRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints\DateTime;
/**
* #ORM\Entity(repositoryClass=OfferRepository::class)
*/
class Offer
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $title;
/**
* #ORM\Column(type="date", name="offer_date")
*/
private $date;
/**
* #ORM\Column(type="string", unique=true)
*/
private $number;
/**
* #ORM\Column(type="string")
*/
private $description;
/**
* #ORM\OneToMany(targetEntity=TermAssign::class, mappedBy="offer")
*/
private $terms;
public function __construct()
{
$this->terms = new ArrayCollection();
}
/**
* Getting offer's id
*/
public function getId(): ?int
{
return $this->id;
}
/**
* getting offer's Title
*/
public function getTitle(): string
{
return $this->title;
}
/**
* Setting offer's name
*/
public function setTitle($title): self
{
$this->title = $title;
return $this;
}
/**
* getting offer's date
*/
public function getDate(): ?\DateTime
{
return $this->date;
}
/**
* Setting offer's date
*/
public function setDate($date): self
{
$this->date = $date;
return $this;
}
/**
* Offer's number
*/
public function getNumber() : string
{
return $this->number;
}
/**
* Setting offer's number
*/
public function setNumber($number): self
{
$this->number = $number;
return $this;
}
/**
* Offer's description
*/
public function getDescription()
{
return $this->description;
}
/**
* Setting offer's description
*/
public function setDescription($description): void
{
$this->description = $description;
}
/**
* #return Collection|TermAssign[]
*/
public function getTerms(): Collection
{
return $this->terms;
}
public function addTerm(TermAssign $term): self
{
if (!$this->terms->contains($term)) {
$this->terms[] = $term;
$term->setOffer($this);
}
return $this;
}
public function removeTerm(TermAssign $term): self
{
if ($this->terms->removeElement($term)) {
// set the owning side to null (unless already changed)
if ($term->getOffer() === $this) {
$term->setOffer(null);
}
}
return $this;
}
}
And this is TermAssign.php:
<?php
namespace App\OfferBundle\Entity;
use App\OfferBundle\Repository\TermAssignRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=TermAssignRepository::class)
*/
class TermAssign
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity=Offer::class, inversedBy="terms")
*/
private $offer;
/**
* #ORM\ManyToOne(targetEntity=Term::class, inversedBy="assign")
*/
private $term;
public function getId(): ?int
{
return $this->id;
}
public function getOffer(): ?Offer
{
return $this->offer;
}
public function setOffer(?Offer $offer): self
{
$this->offer = $offer;
return $this;
}
public function getTerm(): ?Term
{
return $this->term;
}
public function setTerm(?Term $term): self
{
$this->term = $term;
return $this;
}
}
I did came up with query of this sort:
/**
* #return Term[] Returns an array of Term objects
*/
public function findByIdField():array
{
$em = $this->getEntityManager();
$query = $em->createQuery("SELECT t, a FROM App\OfferBundle\Entity\Term t JOIN t.term a WHERE a.term_id = t.id");
return $query->getResult();
}
But it's of no use.
Also, this is part of my controller where I invoke query to be passed into the template:
OfferController.php
/**
* #Route("/{id}", name="offer_show", methods={"GET"})
*/
public function show(Offer $offer, int $id): Response
{
$termAssigns = $this->getDoctrine()
->getRepository(TermAssign::class)
->findBy(
['offer'=>$id]
);
$terms = $this->getDoctrine()
->getRepository(Term::class)
->findByIdField();
$conditions = $this->getDoctrine()
->getRepository(Condition::class)
->findAll();
return $this->render('offer/show.html.twig', [
'offer' => $offer,
'terms'=> $terms,
'conditions'=>$conditions,
'termAssigns'=>$termAssigns,
]);
}
The question is, what can I do to achieve something like
SELECT description FROM Term.t where 't.id'='ta.term_id'
'ta' is TermAssign table.
That was WAY much less complicated than I have imagined... I didn't have to create a new query, all I had to do was to import a TermAssign repository to show() function located in OfferController.php and use findBy() function to fetch terms from Term table. Solution below:
OfferController.php:
/**
* #Route("/{id}", name="offer_show", methods={"GET"})
*/
public function show(Offer $offer, int $id, TermAssign $terms): Response
{
**$termAssigns = $this->getDoctrine()
->getRepository(TermAssign::class)
->findBy(
['offer'=>$id]
);
$terms = $this->getDoctrine()
->getRepository(Term::class)
->findBy(
['id'=>$terms->getId()]
);**
$conditions = $this->getDoctrine()
->getRepository(Condition::class)
->findAll();
return $this->render('offer/show.html.twig', [
'offer' => $offer,
'terms'=> $terms,
'conditions'=>$conditions,
'termAssigns'=>$termAssigns,
]);
}

Semantical Error line 0, col 97 near 'prix WHERE prix': Error: Class App\Entity\Produit has no association named prix

I try to join two entities with Symfony QueryBuilder. I've created a function findAllQuery in order to filter a research, but I get the error that my entity doesn't have any association. I saw already some similar questions but I can't resolve the problem.
Thanks for help !
Publication entity
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=PublicationRepository::class)
*/
class Publication
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="date", nullable=true)
*/
private $datePublication;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="publications")
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity=Produit::class, inversedBy="publications")
*/
private $produit;
public function getId(): ?int
{
return $this->id;
}
public function getDatePublication(): ?\DateTimeInterface
{
return $this->datePublication;
}
public function setDatePublication(?\DateTimeInterface $datePublication): self
{
$this->datePublication = $datePublication;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
public function getProduit(): ?Produit
{
return $this->produit;
}
public function setProduit(?Produit $produit): self
{
$this->produit = $produit;
return $this;
}
}
Produit Entity
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ProduitRepository::class)
*/
class Produit
{
public function hydrate(array $init)
{
foreach ($init as $key => $value) {
$method = "set" . ucfirst($key);
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $nom;
/**
* #ORM\Column(type="string", length=255)
*/
private $description;
/**
* #ORM\Column(type="boolean", nullable=true)
*/
private $vendu;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $photo;
/**
* #ORM\ManyToOne(targetEntity=Commande::class, inversedBy="produits")
*/
private $commande;
/**
* #ORM\ManyToOne(targetEntity=Type::class, inversedBy="produit")
*/
private $type;
/**
* #ORM\OneToOne(targetEntity=Publication::class, mappedBy="produit", cascade={"persist", "remove"})
*/
private $publication;
/**
* #ORM\Column(type="float")
*/
private $prix;
/**
* #ORM\OneToMany(targetEntity=Publication::class, mappedBy="produit")
*/
private $publications;
public function __construct()
{
$this->publications = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function getVendu(): ?bool
{
return $this->vendu;
}
public function setVendu(?bool $vendu): self
{
$this->vendu = $vendu;
return $this;
}
public function getPhoto(): ?string
{
return $this->photo;
}
public function setPhoto(?string $photo): self
{
$this->photo = $photo;
return $this;
}
public function getCommande(): ?Commande
{
return $this->commande;
}
public function setCommande(?Commande $commande): self
{
$this->commande = $commande;
return $this;
}
public function getType(): ?Type
{
return $this->type;
}
public function setType(?Type $type): self
{
$this->type = $type;
return $this;
}
public function getPrix(): ?float
{
return $this->prix;
}
public function setPrix(float $prix): self
{
$this->prix = $prix;
return $this;
}
/**
* #return Collection|Publication[]
*/
public function getPublications(): Collection
{
return $this->publications;
}
public function addPublication(Publication $publication): self
{
if (!$this->publications->contains($publication)) {
$this->publications[] = $publication;
$publication->setProduit($this);
}
return $this;
}
public function removePublication(Publication $publication): self
{
if ($this->publications->removeElement($publication)) {
// set the owning side to null (unless already changed)
if ($publication->getProduit() === $this) {
$publication->setProduit(null);
}
}
return $this;
}
}
Publication Repository
<?php
namespace App\Repository;
use App\Entity\Publication;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use App\Entity\ProduitSearch;
use App\Entity\Produit;
/**
* #method Publication|null find($id, $lockMode = null, $lockVersion = null)
* #method Publication|null findOneBy(array $criteria, array $orderBy = null)
* #method Publication[] findAll()
* #method Publication[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PublicationRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Publication::class);
}
public function findAllQuery (ProduitSearch $search){
if ($search->getMaxPrix()){
return $this
->createQueryBuilder('publication')
->select('produit')
->from('App\Entity\Produit','produit')
->join('produit.prix','prix')
->andWhere('prix <= :maxPrix')
->setParameter('maxPrix', $search->getMaxPrix())
->getQuery()
->getResult();
}
return $this->findAll();
}
Your query is not right. You can only join on relations and not on fields. I guess you try to get publications where the price of any produit is lower than maxPrix.
return $this
->createQueryBuilder('publication')
->join('publication.produit','produit')
->andWhere('produit.prix <= :maxPrix')
->setParameter('maxPrix', $search->getMaxPrix())
->getQuery()
->getResult();

How to refactor Symfony 5 controller to comply with SOLID design principles

I am trying to create this API endpoint that will accept JSON payload and will calculate quote based on provided factors and their ratings.
I have Entities that contain ratings about
"age"
"postcode"
"ABI code"
These AgeRating, PostcodeRating and AbiRating entities implement RatingFactorInterface to force implementation of getRatingFactor() method.
QuoteController seems to be violating "Single Responsibility" and "Open/Close" design principles as the factors like "age", "postcode" can change - extra factor can be added or one of the factors might not be used.
I was thinking maybe it would be possible for rating factors to be specified in the dependency injection container, but can't seem to find a good example how this would work especially with factors that depend on other services like AbiCodeRating which also depends on ABI code which is returned by using third party API which accepts car registration number.
How do I refactor the controller and services so I'm not violating Single Responsibility and Open / Close design principles?
POST JSON Payload example
{
"age": 20,
"postcode": "PE3 8AF",
"regNo": "PJ63 LXR"
}
QuoteController
<?php
namespace App\Controller;
use App\Repository\AbiCodeRatingRepository;
use App\Repository\AgeRatingRepository;
use App\Repository\PostcodeRatingRepository;
use App\Service\AbiCodeLookup;
use App\Service\QuoteCalculator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class QuoteController
* #package App\Controller
*/
class QuoteController extends AbstractController
{
/**
* #Route("/", name="quote")
*
* #param Request $request
* #param AbiCodeRatingRepository $abiCodeRatingRepository
* #param AgeRatingRepository $ageRatingRepository
* #param PostcodeRatingRepository $postcodeRatingRepository
* #return JsonResponse
*/
public function index(Request $request, AbiCodeRatingRepository $abiCodeRatingRepository, AgeRatingRepository $ageRatingRepository, PostcodeRatingRepository $postcodeRatingRepository)
{
try{
$request = $this->transformJsonBody($request);
/**
* Quoting engine could be used with a different set of rating factors!
* Meaning age, postcode and regNo maybe not be required, some other rating factors might be introduced
* How to make controller to accept rating factors dynamically?
*/
if (!$request || !$request->get('age') || !$request->request->get('postcode') || !$request->get('regNo')){
throw new \Exception();
}
/**
* call to a third party API to look up the vehicle registration number and return an ABI code
* this is only required if AbiRating is used with the quoting engine
*/
$abiCode = AbiCodeLookup::getAbiCode($request->get('regNo'));
/**
* $abiCode is only required if postcodeRating is used by quoting engine
*/
$ratingFactors[] = $abiCodeRatingRepository->findOneBy(["abiCode"=>$abiCode]);
$ratingFactors[] = $ageRatingRepository->findOneBy(["age"=>$request->get("age")]);
/**
* $area is only required if postcodeRating is used by quoting engine
*/
$area = substr($request->get("postcode"),0,3);
$ratingFactors[] = $postcodeRatingRepository->findByPostcodeArea($area);
$premiumTotal = QuoteCalculator::calculate($ratingFactors);
$data = [
'status' => 200,
'success' => "Quote created successfully",
'quote' => $premiumTotal
];
return new JsonResponse($data,200);
}catch (\Exception $e){
$data = [
'status' => 422,
'errors' => "Data is not valid",
];
return new JsonResponse($data,422);
}
}
/**
* #param Request $request
* #return Request
*/
protected function transformJsonBody(Request $request)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
return $request;
}
$request->request->replace($data);
return $request;
}
}
AbiCodeRating
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\AbiCodeRatingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ApiResource(
* collectionOperations={"get","post"},
* itemOperations={"get"}
* )
* #ORM\Entity(repositoryClass=AbiCodeRatingRepository::class)
*/
class AbiCodeRating implements RatingFactorInterface
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=10)
*/
private $abiCode;
/**
* #ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
*/
private $ratingFactor;
public function getAbiCode(): ?string
{
return $this->abiCode;
}
public function setAbiCode(string $abiCode): self
{
$this->abiCode = $abiCode;
return $this;
}
public function getRatingFactor(): ?float
{
return $this->ratingFactor;
}
public function setRatingFactor(?float $ratingFactor): self
{
$this->ratingFactor = $ratingFactor;
return $this;
}
}
AgeRating
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\AgeRatingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ApiResource(
* collectionOperations={"get","post"},
* itemOperations={"get"}
* )
* #ORM\Entity(repositoryClass=AgeRatingRepository::class)
*/
class AgeRating implements RatingFactorInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
private $age;
/**
* #ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
*/
private $ratingFactor;
public function getAge(): ?int
{
return $this->age;
}
public function setAge(int $age): self
{
$this->age = $age;
return $this;
}
public function getRatingFactor(): ?float
{
return $this->ratingFactor;
}
public function setRatingFactor(?float $ratingFactor): self
{
$this->ratingFactor = $ratingFactor;
return $this;
}
}
PostcodeRating
<?php
namespace App\Entity;
use App\Repository\PostcodeRatingRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=PostcodeRatingRepository::class)
*/
class PostcodeRating implements RatingFactorInterface
{
/**
* #ORM\Id
* #ORM\Column(type="string", length=4)
*/
private $postcodeArea;
/**
* #ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
*/
private $ratingFactor;
public function getPostcodeArea(): ?string
{
return $this->postcodeArea;
}
public function setPostcodeArea(string $postcodeArea): self
{
$this->postcodeArea = $postcodeArea;
return $this;
}
public function getRatingFactor(): ?float
{
return $this->ratingFactor;
}
public function setRatingFactor(?float $ratingFactor): self
{
$this->ratingFactor = $ratingFactor;
return $this;
}
}
RatingFactorInterface
<?php
namespace App\Entity;
interface RatingFactorInterface
{
/**
* #return float|null
*/
public function getRatingFactor(): ?float;
}
AbiCodeRatingRepository
<?php
namespace App\Repository;
use App\Entity\AbiCodeRating;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #method AbiCodeRating|null find($id, $lockMode = null, $lockVersion = null)
* #method AbiCodeRating|null findOneBy(array $criteria, array $orderBy = null)
* #method AbiCodeRating[] findAll()
* #method AbiCodeRating[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AbiCodeRatingRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AbiCodeRating::class);
}
}
AgeRatingRepository
<?php
namespace App\Repository;
use App\Entity\AgeRating;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #method AgeRating|null find($id, $lockMode = null, $lockVersion = null)
* #method AgeRating|null findOneBy(array $criteria, array $orderBy = null)
* #method AgeRating[] findAll()
* #method AgeRating[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AgeRatingRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AgeRating::class);
}
}
PostcodeRatingRepository
<?php
namespace App\Repository;
use App\Entity\PostcodeRating;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* #method PostcodeRating|null find($id, $lockMode = null, $lockVersion = null)
* #method PostcodeRating|null findOneBy(array $criteria, array $orderBy = null)
* #method PostcodeRating[] findAll()
* #method PostcodeRating[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PostcodeRatingRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, PostcodeRating::class);
}
/**
* #return PostcodeRating Returns PostcodeRating objects based on area
*/
public function findByPostcodeArea($area): ?PostcodeRating
{
return $this->findOneBy(["postcodeArea"=>$area]);
}
}
AbiCodeLookup
<?php
namespace App\Service;
class AbiCodeLookup
{
public static function getAbiCode(string $regNumber){
/**
* create a request to third party api which would return abi code
* How to configure this service to be used only with regNo factor
*/
return "22529902";
}
}
QuoteCalculator
<?php
namespace App\Service;
use App\Entity\RatingFactorInterface;
/**
* Class QuoteCalculator
*/
class QuoteCalculator
{
/**
* #param array $ratingFactors
* #return float
*/
public static function calculate(array $ratingFactors): float
{
$premiumTotal = 500;
foreach ($ratingFactors as $ratingFactor){
$premiumTotal = $premiumTotal * ($ratingFactor instanceof RatingFactorInterface ? $ratingFactor->getRatingFactor() : 1);
}
return $premiumTotal ;
}
}

Symfony authenticator: In controllers, Doctrine returns user item with empty string field, although a value is set

My database schema consists mainly of the following entities: user and auth_token. Each user can have multiple auth_token's.
The problem: when selecting the currently authenticated user in a controller, the string field saltedPasswordHash is empty (""), although a value is set in the database. Getting saltedPasswordHash in the ApiKeyAuthenticator.php works (please have a look at the two TODO comments).
For whatever reason, selecting the email (string) or created (datetime) field works. Persisting new user entities with a saltedPasswordHash or selecting any other user item works fine.
An APIKeyAuthenticator is handling the authorization. When disabling firewall and authentication, everything works as expected. I included the source files below.
I'm using PHP 7.2.15-1 with mysql Ver 15.1 Distrib 10.3.13-MariaDB.
Security/ApiKeyAuthenticator.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
Security/ApiKeyUserProvider.php
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* #param string $authToken
* #return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* #ORM\Column(type="string", length=255, nullable=true, unique=true)
* #Assert\Length(max=255)
* #Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* #ORM\Column(type="string", length=255, nullable=true)
* #Assert\Length(max=255)
* #Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* #ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* #ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* #ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* #return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* #param ArrayCollection $authTokens
* #return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* #param AuthToken $authToken
* #return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* #param AuthToken $authToken
* #return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* #return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* #return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* #return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* #return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* #param mixed $role
* #return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* #return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* #param UserInterface $user
* #return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
Entitiy/AuthToken.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* #ORM\Id()
* #ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* #ORM\Column(type="datetime")
*/
private $added;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* #ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #param mixed $id
* #throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* #return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* #param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* #return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* #param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* #return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* #param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* #return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
This function of your Entity/User.php is the reason of your behavior:
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
When authentication comes in play on Symfony, after the authentication, the AuthenticationProviderManager would call that eraseCredentials function, so it doesn't leak sensible information, or even worse, the sensible information does not end up in your session.
Just try to comment the setter in that function and you should have what you expect.

Symfony, oneToMany relationship Invalid argument supplied for foreach()

I'm adding oneToMany relationship to database, and its working but when i want to view array i get this error :
Warning: Invalid argument supplied for foreach()
this is my User.php
class User implements AdvancedUserInterface, \Serializable
{
// ...
/**
* #ORM\OneToMany(targetEntity="Acme\CityBundle\Entity\City", mappedBy="user")
**/
private $cities;
public function __construct()
{
$this->cities = new ArrayCollection();
}
/**
* #inheritDoc
*/
public function getCities()
{
$c = array();
foreach ($this->cities as $city) {
$c[] = $city->getCity();
}
return $c;
}
/**
* Add cities
*
* #param \Acme\CityBundle\Entity\City $cities
* #return User
*/
public function addCity(\Acme\CityBundle\Entity\City $city)
{
$city->setUser($this);
$this->cities->add($city);
return $this->cities;
}
and City.php
<?php
namespace Acme\CityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* City
*/
class City
{
/**
* #var integer
*/
private $id;
/**
* #var string
*/
private $name;
/**
* #ManyToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="cities")
* #JoinColumn(name="id", referencedColumnName="id")
**/
private $user;
public function setUser($user)
{
$this->user = $user;
return $this;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return City
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getCity()
{
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
}
Controller
$city=new City();
$em = $this->getDoctrine()->getManager();
$city->setName('Miasto');
$user->addCity($city);
$em->persist($city);
$em->persist($user);
$em->flush();
index controller
public function indexAction()
{
$cities=$this->get('security.context')->getToken()->getUser()->getCities();
return $this->render(
'AcmeUserBundle:User:index.html.twig',array( 'cities'=>$cities));
}
and when i try to show array (cities) in view i get error
Warning: Invalid argument supplied for foreach()
in User.php
foreach ($this->cities as $city)
my table city look (id,name)
Thanks for any help.
EDIT : SOLVED
I have to update my CityBundle\Resources\config\doctrine\City.orm.yml file
In your City class, shouldn't be the relationship something like :
/**
* #ManyToOne(targetEntity="Acme\UserBundle\Entity\User", inversedBy="cities")
* #JoinColumn(name="cityId", referencedColumnName="id")
**/
private $user;
The join column shouldn't be the id of user's table.
And then, of course:
public function getCities()
{
return $this->cities;
}

Resources