PHP/MVC/PDO - 在数据库外开始事务 class
PHP/MVC/PDO - beginTransaction outside of Database class
有人可以帮我解决这个问题吗?我有以下 classes(所有功能,为了便于阅读,此处缩写):
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();
}
和一个 class 比方说书籍;
class Books extends Controller {
public function __construct() {
$this->model = $this->loadModel('BookModel');
}
// etc.
$this->model->beginTransaction();
BookModel 看起来像:
class BookModel {
protected $db;
public function __construct() {
$this->db = new Database;
}
public function beginTransaction() {
$this->db->beginTransaction();
}
我知道我只能访问数据库内部的PDO beginTransaction class,但是有没有其他方法,或者我必须使用这个复杂的路径,调用调用PDO的方法的方法方法?
我有一种感觉,我正在做一些非常愚蠢的事情。也许将 BookModel 扩展到数据库 class,但感觉也不对。
谢谢!
一些建议:
[a] 你不应该在 class 方法中创建对象(使用 "new")。相反,您应该将现有实例注入 constructors/setters。这被命名为 dependency injection 并且可以与 dependency injection container.
一起应用
- Dependency Injection and Dependency Inversion in PHP - James Mallison - PHPTour 2017 Nantes
- PHP-DI The dependency injection container for humans
[b] 正如@YourCommonSense 指出的那样,Database
将从在构造函数中注入的单个 PDO 实例中受益匪浅。注入任务将是 DI 容器的工作。例如,如果您使用 PHP-DI, there would be a definition entry 创建数据库连接:
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;
},
];
和另一个 定义 条目将其注入 Database
:
return [
Database::class => autowire()
->constructorParameter('connection', get('database-connection')),
];
Database
构造器看起来像:
public function __construct(PDO $connection) {
$this->dbh = $connection;
}
[c] model 不是 class(像 BookModel
)。它是一层(模型层,或domain model), composed of multiple components: entities (or domain objects), value objects, data mappers, repositories,领域服务。你的BookModel
是一个组合顺便说一句。实体和数据映射器(至少)。注意:从 Database
继承它是错误的,因为模型不能是数据库。
- How should a model be structured in MVC?
- The difference between domains, domain models, object models and domain objects
[d] 你不应该将模型注入控制器。相反,控制器应该使用所谓的application services(也称为用例,或actions,或interactors).这些服务包含所谓的应用程序逻辑,并且是解耦表示层(或交付机制) - 除其他组件外,还包括域模型中的控制器和视图。顺便说一下,应用程序服务还可以确保通信。两层。注意:也可能有 域服务,特定于域并与特定于应用程序的应用程序服务分开。
- 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 根本不需要!您已经拥有非常优雅和强大的 PDO 来处理数据库操作。
[f]其实调用调用PDO方法的方法并没有错。此链中的每个方法都封装了特定于当前对象的特定行为。但是,每种方法的功能都应该增加一些附加值。否则,拥有这条链就没有意义了。一个例子:在一个应用服务中,你可以直接使用一个data mapper从数据库中通过id获取一本书:
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;
}
}
现在,您可以使用 存储库 来隐藏查询数据来自数据库的事实。这是有道理的,因为存储库对象被其他组件视为特定类型(此处 Book
)的对象集合。因此,其他组件认为存储库是书籍的集合,而不是某个数据库中的一堆数据,并相应地向存储库请求它们。存储库将反过来询问数据映射器以查询数据库。所以,之前的代码变成:
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] 一个“真正的”错误是将一个对象传递给其他对象,只供最后一个对象使用。
替代示例代码:
我写了一些代码来替代你的代码。我希望它能帮助您更好地理解基于 MVC 的应用程序的组件如何协同工作。
重要:注意命名空间SampleMvc/Domain/Model/
:那是域模型。请注意,应用程序服务,例如来自 SampleMvc/App/Service/
的所有组件应该只与域模型组件通信,例如使用来自 SampleMvc/Domain/Model/
的组件(主要是接口),而不是来自 SampleMvc/Domain/Infrastructure/
。反过来,您选择的 DI 容器将负责从 SampleMvc/Domain/Infrastructure/
为应用程序服务使用的 SampleMvc/Domain/Model/
接口注入正确的 class 实现。
注意 SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php
中的方法 updateBook()
。我在其中包含了一个交易代码,以及两个很棒的链接。玩得开心。
项目结构:
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;
}
有人可以帮我解决这个问题吗?我有以下 classes(所有功能,为了便于阅读,此处缩写):
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();
}
和一个 class 比方说书籍;
class Books extends Controller {
public function __construct() {
$this->model = $this->loadModel('BookModel');
}
// etc.
$this->model->beginTransaction();
BookModel 看起来像:
class BookModel {
protected $db;
public function __construct() {
$this->db = new Database;
}
public function beginTransaction() {
$this->db->beginTransaction();
}
我知道我只能访问数据库内部的PDO beginTransaction class,但是有没有其他方法,或者我必须使用这个复杂的路径,调用调用PDO的方法的方法方法?
我有一种感觉,我正在做一些非常愚蠢的事情。也许将 BookModel 扩展到数据库 class,但感觉也不对。
谢谢!
一些建议:
[a] 你不应该在 class 方法中创建对象(使用 "new")。相反,您应该将现有实例注入 constructors/setters。这被命名为 dependency injection 并且可以与 dependency injection container.
一起应用- Dependency Injection and Dependency Inversion in PHP - James Mallison - PHPTour 2017 Nantes
- PHP-DI The dependency injection container for humans
[b] 正如@YourCommonSense 指出的那样,Database
将从在构造函数中注入的单个 PDO 实例中受益匪浅。注入任务将是 DI 容器的工作。例如,如果您使用 PHP-DI, there would be a definition entry 创建数据库连接:
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;
},
];
和另一个 定义 条目将其注入 Database
:
return [
Database::class => autowire()
->constructorParameter('connection', get('database-connection')),
];
Database
构造器看起来像:
public function __construct(PDO $connection) {
$this->dbh = $connection;
}
[c] model 不是 class(像 BookModel
)。它是一层(模型层,或domain model), composed of multiple components: entities (or domain objects), value objects, data mappers, repositories,领域服务。你的BookModel
是一个组合顺便说一句。实体和数据映射器(至少)。注意:从 Database
继承它是错误的,因为模型不能是数据库。
- How should a model be structured in MVC?
- The difference between domains, domain models, object models and domain objects
[d] 你不应该将模型注入控制器。相反,控制器应该使用所谓的application services(也称为用例,或actions,或interactors).这些服务包含所谓的应用程序逻辑,并且是解耦表示层(或交付机制) - 除其他组件外,还包括域模型中的控制器和视图。顺便说一下,应用程序服务还可以确保通信。两层。注意:也可能有 域服务,特定于域并与特定于应用程序的应用程序服务分开。
- 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 根本不需要!您已经拥有非常优雅和强大的 PDO 来处理数据库操作。
[f]其实调用调用PDO方法的方法并没有错。此链中的每个方法都封装了特定于当前对象的特定行为。但是,每种方法的功能都应该增加一些附加值。否则,拥有这条链就没有意义了。一个例子:在一个应用服务中,你可以直接使用一个data mapper从数据库中通过id获取一本书:
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;
}
}
现在,您可以使用 存储库 来隐藏查询数据来自数据库的事实。这是有道理的,因为存储库对象被其他组件视为特定类型(此处 Book
)的对象集合。因此,其他组件认为存储库是书籍的集合,而不是某个数据库中的一堆数据,并相应地向存储库请求它们。存储库将反过来询问数据映射器以查询数据库。所以,之前的代码变成:
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] 一个“真正的”错误是将一个对象传递给其他对象,只供最后一个对象使用。
替代示例代码:
我写了一些代码来替代你的代码。我希望它能帮助您更好地理解基于 MVC 的应用程序的组件如何协同工作。
重要:注意命名空间SampleMvc/Domain/Model/
:那是域模型。请注意,应用程序服务,例如来自 SampleMvc/App/Service/
的所有组件应该只与域模型组件通信,例如使用来自 SampleMvc/Domain/Model/
的组件(主要是接口),而不是来自 SampleMvc/Domain/Infrastructure/
。反过来,您选择的 DI 容器将负责从 SampleMvc/Domain/Infrastructure/
为应用程序服务使用的 SampleMvc/Domain/Model/
接口注入正确的 class 实现。
注意 SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php
中的方法 updateBook()
。我在其中包含了一个交易代码,以及两个很棒的链接。玩得开心。
项目结构:
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;
}