I have a scenario like this:
The system handles reservations from couple of different sources. All bookings come in the form of a json with differing formats for each source. Some JSON attributes are present in one JSON and not in the other etc.
In order to enter the reservation into the system, I am trying to centralize the Add Reservation logic into one method. To do this, I am trying to create a class called Reservation using the Builder pattern as such:
<?php
namespace App\Business\Classes\Reservation;
use App\Business\Classes\Utils\ReservationUtils;
/**
* Class Reservation
* Holds entire reservation details
* #package App\Business\Classes\Reservation
*/
class Reservation
{
protected $reference, $otaReference, $hotelCode, $customerId, $guestName, $couponId, $checkin, $checkout, $guests, $customerCountry,
$commissionRate, $paymentStatus, $commissionOta, $commissionDifference, $rooms = [], $cityTax, $vat, $serviceCharge, $geniusBooker = false,
$geniusAmount = 0, $taxes, $taxIncluded = false, $supplierAmount, $totalAmount, $specialRequest, $currency, $deviceType, $bookingSource,
$otaRefId, $status, $source, $remarks;
public function __construct()
{
// generates a reference
$this->reference = ReservationUtils::referenceGenerator();
}
/**
* #param mixed $otaReference
* #return Reservation
*/
public function setOtaReference($otaReference): Reservation
{
$this->otaReference = $otaReference;
return $this;
}
/**
* #param mixed $checkin
* #return Reservation
*/
public function setCheckin($checkin): Reservation
{
$this->checkin = $checkin;
return $this;
}
/**
* #param mixed $checkout
* #return Reservation
*/
public function setCheckout($checkout): Reservation
{
$this->checkout = $checkout;
return $this;
}
/**
* #param array $rooms
* #return Reservation
*/
public function setRooms(array $rooms): Reservation
{
$this->rooms = $rooms;
return $this;
}
// and so on....
}
And I would use it like this:
$data= json_decode($result->jsonData);
$reservation = (new Reservation())
->setOtaReference($data->otaReference)
->setCheckin($data->checkin)
->setCheckout($data->checkout); //...etc
This is probably not the textbook usage of the builder pattern in PHP or even Java but is there anything wrong with it?
Thanks
Related
I am new to OOP/MVC and I do have a basic understanding of a controller which interacts with an underlying model. Basically, a controller acts as a "CRUD gateway" to a model. However, consider an e-commerce marketplace object: Order.
An e-commerce order in a marketplace would interact with multiple tables and hence an order can be thought of as a join of multiple tables: orders, order_items, order_sellers, order_buyer (and more perhaps).
If I understand it correctly, each one of these tables would have a controller allowing CRUD operations (OrderInfoController, OrderItemController,OrderSellerController,OrderBuyerController etc.).
However, could I also create a controller for 'Orders' which then instantiates the Controller Object for each of the tables involved in an Order?
OrderController {
$this->orderInfo = OrderInfo Object;
$this->orderItems = array of Order Item Objects;
$this->orderSellers = array of Order Seller Objects;
$this->orderBuyer = OrderBuyer Object;
function create($arr_order)
//create the order object by calling each of the member controllers.
function get($orderId)
//get the complete order by order Id....
function update($orderId)
function delete($orderId)
}
I have gone through a few MVC docs but I have not come across a solution to this problem. My question is then: Is this the correct approach to write a controller which interacts with multiple tables?
To
If I understand it correctly, each one of these tables would have a controller allowing CRUD operations [...].
In a web MVC-based application, each request is, indeed, served by a controller (the "C" in "MVC").
Though, the controller delegates the whole processing of the request to one or more application services (e.g. use cases, e.g actions - see resources list below), as part of the service layer. These services interact with the model (the "M" in "MVC"), e.g. domain model, e.g. model layer, which, in turn, interact with the database.
The final result of the processing of the request data, e.g. the response object, is either returned to the controller, in order to be passed and printed on screen by the view (the "V" in "MVC"), or directly to the view, for the same reason.
After watching both videos in the resources list below, you will understand, that the model doesn't need to know anything about the database. So, the components of the model layer (mostly interfaces) should not know where and how the data passed to them by the services is saved. Therefore, the services and the controllers should also know nothing about the database.
All informations regarding the database should be located in data mappers only - as part of the infrastructure layer. These objects should be the only ones understanding the database API. Therefore, the only ones containing and beeing able to execute SQL statements.
To
Is this the correct approach to write a controller which interacts with multiple tables?
No. But it's not a problem. Just keep learning about MVC.
Resources:
Keynote: Architecture the Lost Years by Robert Martin.
Sandro Mancuso : Crafted Design
Here is some code of mine. At first sight, it's maybe a lot of it, but I'm confident, that it will help you to better understand.
For simplicity, follow the definition of the method getAllUsers in the view class SampleMvc\App\View\Template\Users\Users.
First of all, here is a not so important note (yet): In my code, the controller only updates the model layer, and the view only fetches data from the model layer. Only the response returned by the view is, therefore, printed. The controller and the view are called by a class RouteInvoker, like this:
<?php
namespace MyPackages\Framework\Routing;
//...
class RouteInvoker implements RouteInvokerInterface {
//...
public function invoke(RouteInterface $route): ResponseInterface {
$controller = $this->resolveController($route);
$view = $this->resolveView($route);
$parameters = $route->getParameters();
$this->callableInvoker->call($controller, $parameters);
return $this->callableInvoker->call($view, $parameters);
}
//...
}
The result ($response) of RouteInvoker:invoke is printed like this:
$responseEmitter->emit($response);
And from here follows an example of a code invoked by RouteInvoker:invoke:
A controller to handle the users:
<?php
namespace SampleMvc\App\Controller\Users;
use function sprintf;
use SampleMvc\App\Service\Users\{
Users as UserService,
Exception\UserExists,
};
use Psr\Http\Message\ServerRequestInterface;
/**
* A controller to handle the users.
*/
class Users {
/**
*
* #param UserService $userService A service to handle the users.
*/
public function __construct(
private UserService $userService
) {
}
/**
* Add a user.
*
* #param ServerRequestInterface $request A server request.
* #return void
*/
public function addUser(ServerRequestInterface $request): void {
$username = $request->getParsedBody()['username'];
try {
$this->userService->addUser($username);
} catch (UserExists $exception) {
//...
}
}
/**
* Remove all users.
*
* #return void
*/
public function removeAllUsers(): void {
$this->userService->removeAllUsers();
}
}
A view to handle the users:
Notice, that controller and view share the same UserService instance.
<?php
namespace SampleMvc\App\View\Template\Users;
use SampleMvc\App\{
View\Layout\Primary,
Service\Users\Users as UserService,
Components\Service\MainNavigation,
};
use Psr\Http\Message\{
ResponseInterface,
ResponseFactoryInterface,
};
use AlePackages\Template\Renderer\TemplateRendererInterface;
/**
* A view to handle the users.
*/
class Users extends Primary {
/**
*
* #param UserService $userService A service to handle the users.
*/
public function __construct(
ResponseFactoryInterface $responseFactory,
TemplateRendererInterface $templateRenderer,
MainNavigation $mainNavigationService,
private UserService $userService
) {
parent::__construct($responseFactory, $templateRenderer, $mainNavigationService);
}
/**
* Display the list of users.
*
* #return ResponseInterface The response to the current request.
*/
public function default(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Users/Users.html.twig', [
'activeNavItem' => 'Users',
'users' => $this->getAllUsers(),
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Add a user.
*
* #return ResponseInterface The response to the current request.
*/
public function addUser(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Users/Users.html.twig', [
'activeNavItem' => 'Users',
'message' => 'User successfully added',
'users' => $this->getAllUsers(),
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Remove all users.
*
* #return ResponseInterface The response to the current request.
*/
public function removeAllUsers(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Users/Users.html.twig', [
'activeNavItem' => 'Users',
'message' => 'All users successfully removed',
'users' => $this->getAllUsers(),
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Get a list of users.
*
* #return (string|int)[][] The list of users.
*/
private function getAllUsers(): array {
$users = $this->userService->findAllUsers();
$usersFormatted = [];
foreach ($users as $user) {
$usersFormatted[] = [
'id' => $user->getId(),
'username' => $user->getUsername(),
];
}
return $usersFormatted;
}
}
A service to handle the users:
<?php
namespace SampleMvc\App\Service\Users;
use SampleMvc\Domain\Model\User\{
User,
UserCollection,
};
use SampleMvc\App\Service\Users\Exception\UserExists;
/**
* A service to handle the users.
*/
class Users {
/**
*
* #param UserCollection $userCollection A collection of users.
*/
public function __construct(
private UserCollection $userCollection
) {
}
/**
* Find a user by id.
*
* #param int $id An id.
* #return User|null The found user or null.
*/
public function findUserById(int $id): ?User {
return $this->userCollection->findById($id);
}
/**
* Find all users.
*
* #return User[] The list of users.
*/
public function findAllUsers(): array {
return $this->userCollection->all();
}
/**
* Add a user.
*
* #param string|null $username A username.
* #return User The added user.
*/
public function addUser(?string $username): User {
$user = $this->createUser($username);
return $this->storeUser($user);
}
/**
* Remove all users.
*
* #return void
*/
public function removeAllUsers(): void {
$this->userCollection->clear();
}
/**
* Create a user.
*
* #param string|null $username A username.
* #return User The user.
*/
private function createUser(?string $username): User {
$user = new User();
$user->setUsername($username);
return $user;
}
/**
* Store a user.
*
* #param User $user A user.
* #return User The stored user.
* #throws UserExists A user already exists.
*/
private function storeUser(User $user): User {
if ($this->userCollection->exists($user)) {
throw new UserExists('Username "' . $user->getUsername() . '" already used');
}
return $this->userCollection->store($user);
}
}
An exception indicating that a user already exists:
<?php
namespace SampleMvc\App\Service\Users\Exception;
/**
* An exception indicating that a user already exists.
*/
class UserExists extends \OverflowException {
}
An interface to a collection of users:
Notice, that this is an interface.
Notice, that this interface is a component of the domain model!
Notice, that its implementation (e.g. SampleMvc\Domain\Infrastructure\Repository\User\UserCollection further down below) is not part of the domain model, but of the infrastructure layer!
<?php
namespace SampleMvc\Domain\Model\User;
use SampleMvc\Domain\Model\User\User;
/**
* An interface to a collection of users.
*/
interface UserCollection {
/**
* Find a user by id.
*
* #param int $id An id.
* #return User|null The found user or null.
*/
public function findById(int $id): ?User;
/**
* Get all users from the collection.
*
* #return User[] All users in the collection.
*/
public function all(): array;
/**
* Store a user.
*
* #param User $user A user.
* #return User The stored user.
*/
public function store(User $user): User;
/**
* Check if a user exists in the collection.
*
* #param User $user A user.
* #return bool True if the user exists, or false otherwise.
*/
public function exists(User $user): bool;
/**
* Remove all users from the collection.
*
* #return static
*/
public function clear(): static;
}
A collection of users:
<?php
namespace SampleMvc\Domain\Infrastructure\Repository\User;
use SampleMvc\Domain\Model\User\{
User,
UserCollection as UserCollectionInterface,
};
use SampleMvc\Domain\Infrastructure\Mapper\User\UserMapper;
/**
* A collection of users.
*/
class UserCollection implements UserCollectionInterface {
/**
*
* #param UserMapper $userMapper A user mapper.
*/
public function __construct(
private UserMapper $userMapper
) {
}
/**
* #inheritDoc
*/
public function findById(int $id): ?User {
return $this->userMapper->fetchUserById($id);
}
/**
* #inheritDoc
*/
public function all(): array {
return $this->userMapper->fetchAllUsers();
}
/**
* #inheritDoc
*/
public function store(User $user): User {
return $this->userMapper->saveUser($user);
}
/**
* #inheritDoc
*/
public function exists(User $user): bool {
return $this->userMapper->userExists($user);
}
/**
* #inheritDoc
*/
public function clear(): static {
$this->userMapper->deleteAllUsers();
return $this;
}
}
An interface to a user mapper:
Notice that this is the interface of a data mapper.
<?php
namespace SampleMvc\Domain\Infrastructure\Mapper\User;
use SampleMvc\Domain\Model\User\User;
/**
* An interface to a user mapper.
*/
interface UserMapper {
/**
* Fetch a user by id.
*
* Note: PDOStatement::fetch returns FALSE if no record is found.
*
* #param int $id A user id.
* #return User|null The user or null.
*/
public function fetchUserById(int $id): ?User;
/**
* Fetch all users.
*
* #return User[] The list of users.
*/
public function fetchAllUsers(): array;
/**
* Save a user.
*
* #param User $user A user.
* #return User The saved user.
*/
public function saveUser(User $user): User;
/**
* Check if a user exists.
*
* Note: PDOStatement::fetch returns FALSE if no record is found.
*
* #param User $user A user.
* #return bool True if the user exists, or false otherwise.
*/
public function userExists(User $user): bool;
/**
* Delete all users.
*
* #return static
*/
public function deleteAllUsers(): static;
}
A PDO user mapper:
Notice, that this component is the implementation of a data mapper.
Notice, that this component is the only one understanding the database API. Therefore, the only one containing and beeing able to execute SQL statements.
Notice, that this component is not part of the domain model, but of the infrastructure layer!
(1) Notice, that you can write any SQL statements that you want, including JOIN statements. So, the fetched data can come from multiple tables as well.
(2) Notice also, that the result of a method of this class could be a list of objects of a type defined by you (!), independent of the underlying table(s) data..
The conclusion from (1) and (2) above: The database structure does NOT affect in any way the way in which your application is structured.
<?php
namespace SampleMvc\Domain\Infrastructure\Mapper\User;
use SampleMvc\Domain\{
Model\User\User,
Infrastructure\Mapper\User\UserMapper,
};
use PDO;
/**
* A PDO user mapper.
*/
class PdoUserMapper implements UserMapper {
/**
*
* #param PDO $connection A database connection.
*/
public function __construct(
private PDO $connection
) {
}
/**
* #inheritDoc
*/
public function fetchUserById(int $id): ?User {
$sql = 'SELECT * FROM users WHERE id = :id LIMIT 1';
$statement = $this->connection->prepare($sql);
$statement->execute([
'id' => $id,
]);
$dataArray = $statement->fetch(PDO::FETCH_ASSOC);
return ($dataArray === false) ? null : $this->convertDataArrayToUser($dataArray);
}
/**
* #inheritDoc
*/
public function fetchAllUsers(): array {
$sql = 'SELECT * FROM users';
$statement = $this->connection->prepare($sql);
$statement->execute();
$listOfDataArrays = $statement->fetchAll(PDO::FETCH_ASSOC);
return $this->convertListOfDataArraysToListOfUsers($listOfDataArrays);
}
/**
* #inheritDoc
*/
public function saveUser(User $user): User {
return $this->insertUser($user);
}
/**
* #inheritDoc
*/
public function userExists(User $user): bool {
$sql = 'SELECT COUNT(*) as cnt FROM users WHERE username = :username';
$statement = $this->connection->prepare($sql);
$statement->execute([
':username' => $user->getUsername(),
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data['cnt'] > 0) ? true : false;
}
/**
* #inheritDoc
*/
public function deleteAllUsers(): static {
$sql = 'DELETE FROM users';
$statement = $this->connection->prepare($sql);
$statement->execute();
return $this;
}
/**
* Insert a user.
*
* #param User $user A user.
* #return User The user, with updated id.
*/
private function insertUser(User $user): User {
$sql = 'INSERT INTO users (username) VALUES (:username)';
$statement = $this->connection->prepare($sql);
$statement->execute([
':username' => $user->getUsername(),
]);
$user->setId($this->connection->lastInsertId());
return $user;
}
/**
* Update a user.
*
* #param User $user A user.
* #return User The user.
*/
private function updateUser(User $user): User {
$sql = 'UPDATE users SET username = :username WHERE id = :id';
$statement = $this->connection->prepare($sql);
$statement->execute([
':username' => $user->getUsername(),
':id' => $user->getId(),
]);
return $user;
}
/**
* Convert the given data array to a user.
*
* #param array $dataArray A data array.
* #return User The user.
*/
private function convertDataArrayToUser(array $dataArray): User {
$user = new User();
$user
->setId($dataArray['id'])
->setUsername($dataArray['username'])
;
return $user;
}
/**
* Convert the given list of data arrays to a list of users.
*
* #param array[] $listOfDataArrays A list of data arrays.
* #return User[] The list of users.
*/
private function convertListOfDataArraysToListOfUsers(array $listOfDataArrays): array {
$listOfUsers = [];
foreach ($listOfDataArrays as $dataArray) {
$listOfUsers[] = $this->convertDataArrayToUser($dataArray);
}
return $listOfUsers;
}
}
could someone help me on this? I have following classes (all functional, abbreviated here for sake of legibility):
class Database {
private $host = DB_HOST;
// etc...
public function __construct() {
$dsn = 'mysql:host=' . $this->host . ';dbname=' . $this->dbname;
$options = array(PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);
try {
$this->dbh = new PDO($dsn, $this->user, $this->pass, $options);
} catch (PDOException $e) {
$this->error = $e->getMessage();
echo $this->error;
}
}
public function beginTransaction() {
$this->stmt = $this->dbh->beginTransaction();
}
and a class for let’s say books;
class Books extends Controller {
public function __construct() {
$this->model = $this->loadModel('BookModel');
}
// etc.
$this->model->beginTransaction();
and the BookModel looks like:
class BookModel {
protected $db;
public function __construct() {
$this->db = new Database;
}
public function beginTransaction() {
$this->db->beginTransaction();
}
I know I can only access the PDO beginTransaction inside of the Database class, but is there another way, or I have to use this complicated path, call the method that calls the method that calls the PDO method?
I have a feeling I’m doing something very stupid here. Maybe extending the BookModel to the Database class, but that doesn’t feel right either.
Thanks!
Some suggestions:
[a] You shouldn't create objects (with "new") inside class methods. Instead you should inject existent instances into constructors/setters. This is named dependency injection and can be applied with a dependency injection container.
Dependency Injection and Dependency Inversion in PHP - James Mallison - PHPTour 2017 Nantes
PHP-DI The dependency injection container for humans
[b] As #YourCommonSense noted, Database would greatly benefit from a single PDO instance, injected in the constructor. The injection task would be the job of the DI container. For example, if you'd use PHP-DI, there would be a definition entry for creating a database connection:
return [
'database-connection' => function (ContainerInterface $container) {
$parameters = $container->get('database.connection');
$dsn = $parameters['dsn'];
$username = $parameters['username'];
$password = $parameters['password'];
$connectionOptions = [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$connection = new PDO($dsn, $username, $password, $connectionOptions);
return $connection;
},
];
and another definition entry to inject it in Database:
return [
Database::class => autowire()
->constructorParameter('connection', get('database-connection')),
];
The Database contructor would look like:
public function __construct(PDO $connection) {
$this->dbh = $connection;
}
[c] The model is not a class (like BookModel). It is a layer (model layer, or domain model), composed of multiple components: entities (or domain objects), value objects, data mappers, repositories, domain services. Your BookModel is a combination btw. an entity and a data mapper (at least). Note: inheriting it from Database is wrong, because a model can't be a database.
How should a model be structured in MVC?
The difference between domains, domain models, object models and domain objects
[d] You shouldn't inject models into controllers. Instead, controllers should use so-called application services (also named use cases, or actions, or interactors). These services contain the so-called application logic and are the proper way to decouple the presentation layer (or delivery mechanism) - which, among other components, includes the controllers and the views - from the domain model. The application services also assure the communication btw. the two layers. Note: there could also be domain services, specific to the domain and separated from the application services, which are specific to the application.
Sandro Mancuso : Crafted Design
Ruby Midwest 2011 - Keynote: Architecture the Lost Years by Robert Martin
Robert "Uncle Bob" Martin - Architecture: The Lost Years
How should a model be structured in MVC?
[e] Database class is not needed at all! You already have the very elegant & powerful PDO at disposal, to handle the database operations.
[f] Actually, it is not wrong to "call the method that calls the method that calls the PDO method". Each method in this chain encapsulates a certain behavior, specific to the current object. Though, the functionality of each method should add some plus value. Otherwise, it wouldn't make sense to have this chain, indeed. An example: In an application service, you can directly use a data mapper to fetch a book by id from the database:
class FindBooksService {
public function __construct(
private BookMapper $bookMapper
) {
}
public function findBookById(?int $id = null): ?Book {
return $this->bookMapper->fetchBookById($id);
}
}
class BookMapper {
public function __construct(
private PDO $connection
) {
}
public function fetchBookById(?int $id): ?Book {
$sql = 'SELECT * FROM books WHERE id = :id LIMIT 1';
// Fetch book data from database; convert the record to a Book object ($book).
//...
return $book;
}
}
Now, you could use a repository instead, to hide even the fact that the queried data comes from a database. This makes sense, since a repository object is seen as a collection of objects of a certain type (here Book) by other components. Therefore, the other components think that the repository is a collection of books, not a bunch of data in some database, and they ask the repository for them correspondingly. The repository will, in turn, interogate the data mapper to query the database. So, the previous code becomes:
class FindBooksService {
/**
* #param BookCollection $bookCollection The repository: a collection of books, e.g. of Book instances.
*/
public function __construct(
private BookCollection $bookCollection
) {
}
public function findBookById(?int $id = null): ?Book {
return $this->bookCollection->findBookById($id);
}
}
class BookCollection {
private array $books = [];
public function __construct(
private BookMapper $bookMapper
) {
}
/**
* This method adds a plus value to the omolog method in the data mapper (fetchBookById):
* - caches the Book instances in the $books list, therefore reducing the database querying operations;
* - hides the fact, that the data comes from a database, from the external world, e.g. other components.
* - provides an elegant collection-like interface.
*/
public function findBookById(?int $id): ?Book {
if (!array_key_exists($id, $this->books)) {
$book = $this->bookMapper->fetchBookById($id);
$this->books[id] = $book;
}
return $this->books[$id];
}
}
class BookMapper {
// the same...
}
[g] A "real" mistake would be to pass an object through other objects, just to be used by the last object.
Alternative example code:
I wrote some code as an alternative to yours. I hope it will help you better understand, how the components of an MVC-based application could work together.
Important: Notice the namespace SampleMvc/Domain/Model/: that's the domain model. Note that the application services, e.g. all components from SampleMvc/App/Service/, should communicate ONLY with the domain model components, e.g. with the components from SampleMvc/Domain/Model/ (mostly interfaces), not from SampleMvc/Domain/Infrastructure/. In turn, the DI container of your choice will take care of injecting the proper class implementations from SampleMvc/Domain/Infrastructure/ for the interfaces of SampleMvc/Domain/Model/ used by the application services.
Notice the method updateBook() in SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php. I included a transaction code in it, along with two great links. Have fun.
Project structure:
SampleMvc/App/Controller/Book/AddBook.php:
<?php
namespace SampleMvc\App\Controller\Book;
use Psr\Http\Message\{
ResponseInterface,
ServerRequestInterface,
};
use SampleMvc\App\Service\Book\{
AddBook as AddBookService,
Exception\BookAlreadyExists,
};
use SampleMvc\App\View\Book\AddBook as AddBookView;
/**
* A controller for adding a book.
*
* Let's assume the existence of this route definition:
*
* $routeCollection->post('/books/add', SampleMvc\App\Controller\Book\AddBook::class);
*/
class AddBook {
/**
* #param AddBookView $view The view for presenting the response to the request back to the user.
* #param AddBookService $addBookService An application service for adding a book to the model layer.
*/
public function __construct(
private AddBookView $view,
private AddBookService $addBookService
) {
}
/**
* Add a book.
*
* The book details are submitted from a form, using the HTTP method "POST".
*
* #param ServerRequestInterface $request A server request.
* #return ResponseInterface The response to the current request.
*/
public function __invoke(ServerRequestInterface $request): ResponseInterface {
$authorName = $request->getParsedBody()['authorName'];
$title = $request->getParsedBody()['title'];
try {
$book = $this->addBookService($authorName, $title);
$this->view->setBook($book);
} catch (BookAlreadyExists $exception) {
$this->view->setErrorMessage(
$exception->getMessage()
);
}
$response = $this->view->addBook();
return $response;
}
}
SampleMvc/App/Controller/Book/FindBooks.php:
<?php
namespace SampleMvc\App\Controller\Book;
use Psr\Http\Message\ResponseInterface;
use SampleMvc\App\View\Book\FindBooks as FindBooksView;
use SampleMvc\App\Service\Book\FindBooks as FindBooksService;
/**
* A controller for finding books.
*
* Let's assume the existence of this route definition:
*
* $routeCollection->post('/books/find/{authorName}', [SampleMvc\App\Controller\FindBooks::class, 'findBooksByAuthorName']);
*/
class FindBooks {
/**
* #param FindBooksView $view The view for presenting the response to the request back to the user.
* #param FindBooksService $findBooksService An application service for finding books by querying the model layer.
*/
public function __construct(
private FindBooksView $view,
private FindBooksService $findBooksService
) {
}
/**
* Find books by author name.
*
* The author name is provided by clicking on a link of some author name
* in the browser. The author name is therefore sent using the HTTP method
* "GET" and passed as argument to this method by a route dispatcher.
*
* #param string|null $authorName (optional) An author name.
* #return ResponseInterface The response to the current request.
*/
public function findBooksByAuthorName(?string $authorName = null): ResponseInterface {
$books = $this->findBooksService->findBooksByAuthorName($authorName);
$response = $this->view
->setBooks($books)
->findBooksByAuthorName()
;
return $response;
}
}
SampleMvc/App/Service/Book/Exception/BookAlreadyExists.php:
<?php
namespace SampleMvc\App\Service\Book\Exception;
/**
* An exception thrown if a book already exists.
*/
class BookAlreadyExists extends \OverflowException {
}
SampleMvc/App/Service/Book/AddBook.php:
<?php
namespace SampleMvc\App\Service\Book;
use SampleMvc\Domain\Model\Book\{
Book,
BookMapper,
};
use SampleMvc\App\Service\Book\Exception\BookAlreadyExists;
/**
* An application service for adding a book.
*/
class AddBook {
/**
* #param BookMapper $bookMapper A data mapper for transfering books
* to and from a persistence system.
*/
public function __construct(
private BookMapper $bookMapper
) {
}
/**
* Add a book.
*
* #param string|null $authorName An author name.
* #param string|null $title A title.
* #return Book The added book.
*/
public function __invoke(?string $authorName, ?string $title): Book {
$book = $this->createBook($authorName, $title);
return $this->storeBook($book);
}
/**
* Create a book.
*
* #param string|null $authorName An author name.
* #param string|null $title A title.
* #return Book The newly created book.
*/
private function createBook(?string $authorName, ?string $title): Book {
return new Book($authorName, $title);
}
/**
* Store a book.
*
* #param Book $book A book.
* #return Book The stored book.
* #throws BookAlreadyExists The book already exists.
*/
private function storeBook(Book $book): Book {
if ($this->bookMapper->bookExists($book)) {
throw new BookAlreadyExists(
'A book with the author name "' . $book->getAuthorName() . '" '
. 'and the title "' . $book->getTitle() . '" already exists'
);
}
return $this->bookMapper->saveBook($book);
}
}
SampleMvc/App/Service/Book/FindBooks.php:
<?php
namespace SampleMvc\App\Service\Book;
use SampleMvc\Domain\Model\Book\{
Book,
BookMapper,
};
/**
* An application service for finding books.
*/
class FindBooks {
/**
* #param BookMapper $bookMapper A data mapper for transfering books
* to and from a persistence system.
*/
public function __construct(
private BookMapper $bookMapper
) {
}
/**
* Find a book by id.
*
* #param int|null $id (optional) A book id.
* #return Book|null The found book, or null if no book was found.
*/
public function findBookById(?int $id = null): ?Book {
return $this->bookMapper->fetchBookById($id);
}
/**
* Find books by author name.
*
* #param string|null $authorName (optional) An author name.
* #return Book[] The found books list.
*/
public function findBooksByAuthorName(?string $authorName = null): array {
return $this->bookMapper->fetchBooksByAuthorName($authorName);
}
}
SampleMvc/App/View/Book/AddBook.php:
<?php
namespace SampleMvc\App\View\Book;
use SampleMvc\{
App\View\View,
Domain\Model\Book\Book,
};
use Psr\Http\Message\ResponseInterface;
/**
* A view for adding a book.
*/
class AddBook extends View {
/** #var Book The added book. */
private Book $book = null;
/**
* Add a book.
*
* #return ResponseInterface The response to the current request.
*/
public function addBook(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Book/AddBook.html.twig', [
'activeNavItem' => 'AddBook',
'book' => $this->book,
'error' => $this->errorMessage,
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Set the book.
*
* #param Book $book A book.
* #return static
*/
public function setBook(Book $book): static {
$this->book = $book;
return $this;
}
}
SampleMvc/App/View/Book/FindBooks.php:
<?php
namespace SampleMvc\App\View\Book;
use SampleMvc\{
App\View\View,
Domain\Model\Book\Book,
};
use Psr\Http\Message\ResponseInterface;
/**
* A view for finding books.
*/
class FindBooks extends View {
/** #var Book[] The list of found books. */
private array $books = [];
/**
* Find books by author name.
*
* #return ResponseInterface The response to the current request.
*/
public function findBooksByAuthorName(): ResponseInterface {
$bodyContent = $this->templateRenderer->render('#Templates/Book/FindBooks.html.twig', [
'activeNavItem' => 'FindBooks',
'books' => $this->books,
]);
$response = $this->responseFactory->createResponse();
$response->getBody()->write($bodyContent);
return $response;
}
/**
* Set the books list.
*
* #param Book[] $books A list of books.
* #return static
*/
public function setBooks(array $books): static {
$this->books = $books;
return $this;
}
}
SampleMvc/App/View/View.php:
<?php
namespace SampleMvc\App\View;
use Psr\Http\Message\ResponseFactoryInterface;
use SampleLib\Template\Renderer\TemplateRendererInterface;
/**
* View.
*/
abstract class View {
/** #var string The error message */
protected string $errorMessage = '';
/**
* #param ResponseFactoryInterface $responseFactory Response factory.
* #param TemplateRendererInterface $templateRenderer Template renderer.
*/
public function __construct(
protected ResponseFactoryInterface $responseFactory,
protected TemplateRendererInterface $templateRenderer
) {
}
/**
* Set the error message.
*
* #param string $errorMessage An error message.
* #return static
*/
public function setErrorMessage(string $errorMessage): static {
$this->errorMessage = $errorMessage;
return $this;
}
}
SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php:
<?php
namespace SampleMvc\Domain\Infrastructure\Book;
use SampleMvc\Domain\Model\Book\{
Book,
BookMapper,
};
use PDO;
/**
* A data mapper for transfering Book entities to and from a database.
*
* This class uses a PDO instance as database connection.
*/
class PdoBookMapper implements BookMapper {
/**
* #param PDO $connection Database connection.
*/
public function __construct(
private PDO $connection
) {
}
/**
* #inheritDoc
*/
public function bookExists(Book $book): bool {
$sql = 'SELECT COUNT(*) as cnt FROM books WHERE author_name = :author_name AND title = :title';
$statement = $this->connection->prepare($sql);
$statement->execute([
':author_name' => $book->getAuthorName(),
':title' => $book->getTitle(),
]);
$data = $statement->fetch(PDO::FETCH_ASSOC);
return ($data['cnt'] > 0) ? true : false;
}
/**
* #inheritDoc
*/
public function saveBook(Book $book): Book {
if (isset($book->getId())) {
return $this->updateBook($book);
}
return $this->insertBook($book);
}
/**
* #inheritDoc
*/
public function fetchBookById(?int $id): ?Book {
$sql = 'SELECT * FROM books WHERE id = :id LIMIT 1';
$statement = $this->connection->prepare($sql);
$statement->execute([
'id' => $id,
]);
$record = $statement->fetch(PDO::FETCH_ASSOC);
return ($record === false) ?
null :
$this->convertRecordToBook($record)
;
}
/**
* #inheritDoc
*/
public function fetchBooksByAuthorName(?string $authorName): array {
$sql = 'SELECT * FROM books WHERE author_name = :author_name';
$statement = $this->connection->prepare($sql);
$statement->execute([
'author_name' => $authorName,
]);
$recordset = $statement->fetchAll(PDO::FETCH_ASSOC);
return $this->convertRecordsetToBooksList($recordset);
}
/**
* Update a book.
*
* This method uses transactions as example.
*
* Note: I never worked with transactions, but I
* think the code in this method is not wrong.
*
* #link https://phpdelusions.net/pdo#transactions (The only proper) PDO tutorial: Transactions
* #link https://phpdelusions.net/pdo (The only proper) PDO tutorial
* #link https://phpdelusions.net/articles/error_reporting PHP error reporting
*
* #param Book $book A book.
* #return Book The updated book.
* #throws \Exception Transaction failed.
*/
private function updateBook(Book $book): Book {
$sql = 'UPDATE books SET author_name = :author_name, title = :title WHERE id = :id';
try {
$this->connection->beginTransaction();
$statement = $this->connection->prepare($sql);
$statement->execute([
':author_name' => $book->getAuthorName(),
':title' => $book->getTitle(),
':id' => $book->getId(),
]);
$this->connection->commit();
} catch (\Exception $exception) {
$this->connection->rollBack();
throw $exception;
}
return $book;
}
/**
* Insert a book.
*
* #param Book $book A book.
* #return Book The newly inserted book.
*/
private function insertBook(Book $book): Book {
$sql = 'INSERT INTO books (author_name, title) VALUES (:author_name, :title)';
$statement = $this->connection->prepare($sql);
$statement->execute([
':author_name' => $book->getAuthorName(),
':title' => $book->getTitle(),
]);
$book->setId(
$this->connection->lastInsertId()
);
return $book;
}
/**
* Convert the given record to a Book instance.
*
* #param array $record The record to be converted.
* #return Book A Book instance.
*/
private function convertRecordToBook(array $record): Book {
$id = $record['id'];
$authorName = $record['author_name'];
$title = $record['title'];
$book = new Book($authorName, $title);
$book->setId($id);
return $book;
}
/**
* Convert the given recordset to a list of Book instances.
*
* #param array $recordset The recordset to be converted.
* #return Book[] A list of Book instances.
*/
private function convertRecordsetToBooksList(array $recordset): array {
$books = [];
foreach ($recordset as $record) {
$books[] = $this->convertRecordToBook($record);
}
return $books;
}
}
SampleMvc/Domain/Model/Book/Book.php:
<?php
namespace SampleMvc\Domain\Model\Book;
/**
* Book entity.
*/
class Book {
/**
* #param string|null $authorName (optional) The name of an author.
* #param string|null $title (optional) A title.
*/
public function __construct(
private ?string $authorName = null,
private ?string $title = null
) {
}
/**
* Get id.
*
* #return int|null
*/
public function getId(): ?int {
return $this->id;
}
/**
* Set id.
*
* #param int|null $id An id.
* #return static
*/
public function setId(?int $id): static {
$this->id = $id;
return $this;
}
/**
* Get the author name.
*
* #return string|null
*/
public function getAuthorName(): ?string {
return $this->authorName;
}
/**
* Set the author name.
*
* #param string|null $authorName The name of an author.
* #return static
*/
public function setAuthorName(?string $authorName): static {
$this->authorName = $authorName;
return $this;
}
/**
* Get the title.
*
* #return string|null
*/
public function getTitle(): ?string {
return $this->title;
}
/**
* Set the title.
*
* #param string|null $title A title.
* #return static
*/
public function setTitle(?string $title): static {
$this->title = $title;
return $this;
}
}
SampleMvc/Domain/Model/Book/BookMapper.php:
<?php
namespace SampleMvc\Domain\Model\Book;
use SampleMvc\Domain\Model\Book\Book;
/**
* An interface for various data mappers used to
* transfer Book entities to and from a persistence system.
*/
interface BookMapper {
/**
* Check if a book exists.
*
* #param Book $book A book.
* #return bool True if the book exists, false otherwise.
*/
public function bookExists(Book $book): bool;
/**
* Save a book.
*
* #param Book $book A book.
* #return Book The saved book.
*/
public function saveBook(Book $book): Book;
/**
* Fetch a book by id.
*
* #param int|null $id A book id.
* #return Book|null The found book, or null if no book was found.
*/
public function fetchBookById(?int $id): ?Book;
/**
* Fetch books by author name.
*
* #param string|null $authorName An author name.
* #return Book[] The found books list.
*/
public function fetchBooksByAuthorName(?string $authorName): array;
}
so, I have a performance issue with my extbase plugin.
The scenario is such:
I have 4 database tables with data for artists, artworks, exhibitions and publications.
Exhibitions can have artists, artworks and publications as mm relations.
publications and artwork can have artists as mm relations.
In my Model Classes I have those relations as ObjectStorage and use simple findAll() Method for my List View.
So if I get to the Exhibition List View I get every Exhibition and their related Artists and all related Artworks/Publications/Exhibitions of that Artist.
The performance is really bad and a not cached page needs almost a full minute to load.
And this is all because of the heavy data load from the db.
I only need the MM relations on the first level and not further. Is there anyway to config this?
Classes\Domain\Model\Exhibition.php
class Exhibition extends AbstractEntity
{
/**
* #var string
*/
protected $name = '';
/**
* #var string
*/
protected $location = '';
/**
* artists
*
* #var ObjectStorage<\Vendor\Project\Domain\Model\Artist>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $artists = null;
/**
* artworks
*
* #var ObjectStorage<\Vendor\Project\Domain\Model\Artwork>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $artworks;
/**
* publications
*
* #var ObjectStorage<\Vendor\Project\Domain\Model\Publication>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $publications;
/**
* Fal media items
*
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference>
* #TYPO3\CMS\Extbase\Annotation\ORM\Lazy
*/
protected $falMedia;
public function __construct(string $name = '', string $description = '', string $location = '')
{
$this->setName($name);
$this->setLocation($location);
}
/**
* #param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #return string
*/
public function getLocation(): string
{
return $this->location;
}
/**
* #param string $location
*/
public function setLocation(string $location): void
{
$this->location = $location;
}
/**
* Returns the artist
*
* #return ObjectStorage<\Vendor\Project\Domain\Model\Artist> $artists
*/
public function getArtists()
{
return $this->artists;
}
/**
* Sets the artist
*
* #param ObjectStorage<\Vendor\GKG\Domain\Model\Artist> $artists
* #return void
*/
public function setArtists(ObjectStorage $artists)
{
$this->artists = $artists;
}
/**
* #param $artworks
*/
public function setArtworks($artworks)
{
$this->artworks = $artworks;
}
/**
* #return ObjectStorage
*/
public function getArtworks()
{
return $this->artworks;
}
/**
* Sets the publications
*
* #param ObjectStorage<\Vendor\Project\Domain\Model\Publication> $oublications
* #return void
*/
public function setPublications(ObjectStorage $publications)
{
$this->publications = $publications;
}
/**
* Returns the publications
*
* #return ObjectStorage<\Vendor\Project\Domain\Model\Publication> $publications
*/
public function getPublications()
{
return $this->publications;
}
/**
* Get the Fal media items
*
* #return \TYPO3\CMS\Extbase\Persistence\ObjectStorage
*/
public function getFalMedia()
{
return $this->falMedia;
}
}
This gets all the Artist data in which all related data is fetched as well (artwork, exhibition and publication). And this is a heavy overload :/
I tried to write my own query and only select the values I need in my frontend:
Classes\Domain\Repository\ExhibitionRepository.php
public function findExhibitions()
{
$languageAspect = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance
(\TYPO3\CMS\Core\Context\Context::class)->getAspect('language');
$sys_language_uid = $languageAspect->getId();
$query = $this->createQuery();
$query->getQuerySettings()->setRespectSysLanguage(true);
$query->statement( "
SELECT DISTINCT e.name, e.uid, e.location, e.fal_media, a.name, a.uid
FROM tx_project_domain_model_exhibition e
LEFT JOIN tx_project_exhibitions_artists_mm
ON tx_project_exhibitions_artists_mm.uid_local = e.uid
LEFT JOIN tx_project_domain_model_artist a
ON tx_project_exhibitions_artists_mm.uid_foreign = a.uid
WHERE e.sys_language_uid = $sys_language_uid
GROUP BY tx_project_exhibitions_artists_mm.uid_local
" );
return $query->execute();
}
But with this approach I’m not able to get the relation data to my assigned view variable inside my Controller. Only the exhibition related stuff.
Classes\Controller\ExhibitionController.php
public function indexAction()
{
$queryResult = $this->exhibitionRepository->findExhibitions();
$this->view->assign('exhibitions', $queryResult);
}
Any insight and advice on how to tackle this problem and only get the needed Data and not everything from the ObjectStorage?
Thanks in advance and best regards
It looks like your query cost is very high with all the JOINS and without a condition on a indexed column.
You could check it with an EXPLAIN PLAN(MySQL) / EXECUTION PLAN(MS).
Set condition with an index to boost Performance
Rather using the Comparing operations leads to Performance.
https://docs.typo3.org/m/typo3/book-extbasefluid/main/en-us/6-Persistence/3-implement-individual-database-queries.html
Another performance boost is to use Ajax, just search for "TYPO3 Ajax":
get an identifier and display it in the Frontend:
SELECT DISTINCT e. uid, e.name FROM tx_project_domain_model_exhibition
On identifier click (name or whatever) make a call of the AjaxController.php:
...
$this->exhibitionRepository->findByUid($uid);
...
or make your own repository comparing
...
$query->matching(
$query->logicalAnd(
$query->equals('uid', $uid),
$query->equals('name', $name)
)
);
...
show the data on a Modal Window,Accordion or a new View.
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)));
}
}
Trying to include a php file in a script and even though the page loads without showing or logging errors, the included file is not executed.
I have created a test file that looks like this:
<?php
$sale_amount = '10.00';
$product = 'Drive-Plan';
include('partners/controller/record-sale.php');
echo "commission counted"
?>
opening this file in browser completes the expected function.
Trying to include this same code in my script does not complete the function.
original script works good:
/**
* Swap current users plan to a new one.
*
* #return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function swapPlan() {
if ($this->user->subscribed() && Input::get('plan')) {
$this->user->subscription(Input::get('plan'))->swap();
return response(trans('app.planSwapSuccess', ['plan' => Input::get('plan')]), 200);
}
}
Including my file in this script does not have the expected effect.
/**
* Swap current users plan to a new one.
*
* #return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function swapPlan() {
if ($this->user->subscribed() && Input::get('plan')) {
$this->user->subscription(Input::get('plan'))->swap();
return response(trans('app.planSwapSuccess', ['plan' => Input::get('plan')]), 200);
$sale_amount = '10.00';
$product = 'Drive-Plan';
include('partners/controller/record-sale.php');
}
}
The page will complete loading without errors, however the function that should be completed by the included file is not executed. Any Ideas on what I may have done wrong are appreciated.
I have spent a few days trying to figure this out. Since my test file works I'm guessing I'm missing something obvious in the full script.
Thanks for looking.
Comeplete file:
<?php namespace App\Http\Controllers;
use App;
use Auth;
use Input;
use Stripe\Stripe;
use Stripe\Plan;
class PaymentsController extends Controller {
public function __construct() {
$this->middleware('loggedIn');
$this->middleware('paymentsEnabled');
$this->user = Auth::user();
$this->settings = App::make('App\Services\Settings');
Stripe::setApiKey($this->settings->get('stripe_secret_key'));
}
/**
* Subscribe user to a plan or swap him to a different plan.
*
* #return response
*/
public function upgrade() {
if ($this->user->subscribed()) {
$this->user->subscription(Input::get('plan'))->swap();
} else {
$this->user->subscription(Input::get('plan'))->create(Input::get('stripe_token'), ['email' => $this->user->email]);
}
return response(trans('app.upgradeSuccess'), 200);
$sale_amount = '10.00';
$product = 'Drive-Plan';
include('partners/controller/record-sale.php');
}
/**
* Swap current users plan to a new one.
*
* #return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function swapPlan() {
if ($this->user->subscribed() && Input::get('plan')) {
$this->user->subscription(Input::get('plan'))->swap();
return response(trans('app.planSwapSuccess', ['plan' => Input::get('plan')]), 200);
$sale_amount = '10.00';
$product = 'Drive-Plan';
include('partners/controller/record-sale.php');
}
}
/**
* Attach new credit card to user.
*
* #return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function addNewCard() {
$this->user->updateCard(Input::get('stripe_token'));
return response(trans('app.cardAddSuccess'), 200);
}
/**
* Resume a canceled subscription.
*/
public function resumeSubscription() {
$this->user->subscription(Input::get('plan'))->resume(Input::get('token'));
return $this->user;
$sale_amount = '10.00';
$product = 'Drive-Plan';
include('partners/controller/record-sale.php');
}
/**
* Cancel users subscription.
*
* #return \App\User
*/
public function unsubscribe() {
$this->user->subscription()->cancel();
return $this->user;
}
/**
* Return current users invoices.
*
* #return array
*/
public function getInvoices() {
return view('invoices')->with('invoices', $this->user->invoices())->with('settings', $this->settings);
}
/**
* Download invoice with given id.
*
* #param {int|string} $id
* #return \Symfony\Component\HttpFoundation\Response
*/
public function downloadInvoice($id) {
return $this->user->downloadInvoice($id, [
'vendor' => $this->settings->get('invoiceVendor'),
'product' => $this->settings->get('invoiceProduct'),
]);
}
/**
* Return all created plans.
*
* #return array
*/
public function getPlans() {
$plans = Plan::all();
$formatted = [];
foreach($plans->data as $plan) {
$formatted[] = [
'interval' => $plan['interval'],
'name' => $plan['name'],
'amount' => $plan['amount'] / 100,
'currency' => $plan['currency'],
'id' => $plan['id'],
'created' => $plan['created'],
];
}
usort($formatted, function($a1, $a2) {
if ($a1['created'] == $a2['created']) return 0;
return ($a1['created'] < $a2['created']) ? -1 : 1;
});
return $formatted;
}
}
Your method ends on return, meaning your include in the methods and variables being set are never reached.
An example:
Moving said include and variables to before the return should solve the issue.
/**
* Resume a canceled subscription.
*/
public function resumeSubscription() {
$this->user->subscription(Input::get('plan'))->resume(Input::get('token'));
// Moved to before return
$sale_amount = '10.00';
$product = 'Drive-Plan';
include('partners/controller/record-sale.php');
return $this->user;
// UNREACHABLE
//$sale_amount = '10.00';
//$product = 'Drive-Plan';
//include('partners/controller/record-sale.php');
}