PHP/MVC/PDO-数据库类外部的beginTransaction

PHP/MVC/PDO-数据库类外部的beginTransaction,php,class,model-view-controller,methods,pdo,Php,Class,Model View Controller,Methods,Pdo,有人能帮我吗?我有以下课程(所有课程都是功能性的,为了便于阅读,此处缩写): 还有一堂书课 class Books extends Controller { public function __construct() { $this->model = $this->loadModel('BookModel'); } // etc. $this->model->beginTransaction(); 图书模型如下所示:

有人能帮我吗?我有以下课程(所有课程都是功能性的,为了便于阅读,此处缩写):

还有一堂书课

  class Books extends Controller {
    public function __construct() {
      $this->model = $this->loadModel('BookModel');
    }

    // etc.
    $this->model->beginTransaction();

图书模型如下所示:

class BookModel {
  protected $db;

  public function __construct() {
    $this->db = new Database;
  }

  public function beginTransaction() {
    $this->db->beginTransaction();
  }
我知道我只能访问数据库类内部的PDO beginTransaction,但是有没有其他方法,或者我必须使用这个复杂的路径,调用调用PDO方法的方法

我有一种感觉,我在做一些非常愚蠢的事情。也许可以将BookModel扩展到Database类,但这感觉也不对


谢谢

一些建议:

[a]您不应该在类方法内部创建对象(使用“new”)。相反,您应该将现有实例注入构造函数/设置器。这被命名为依赖项注入,可以与依赖项注入容器一起应用

[b]正如@YourCommonSense所指出的,
数据库将从注入构造函数的单个PDO实例中获益匪浅。注入任务将是DI容器的工作。例如,如果要使用,将有一个用于创建数据库连接的

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;
    },
];
和另一个定义条目,将其注入
数据库

return [
    Database::class => autowire()
        ->constructorParameter('connection', get('database-connection')),
];
数据库
构造函数如下所示:

public function __construct(PDO $connection) {
    $this->dbh = $connection;
}
[c]模型
不是一个类(如
BookModel
)。它是一个层(模型层,或),由多个组件组成:实体(或域对象),域服务。您的
BookModel
是实体和数据映射器(至少)的组合。注意:从
数据库继承它是错误的,因为模型不能是数据库

[d]您不应该将模型注入控制器。相反,控制器应该使用所谓的(也称为用例,或动作,或交互者)。这些服务包含所谓的应用程序逻辑,是将表示层(或交付机制)与域模型解耦的正确方法,其中包括控制器和视图。应用服务还保证了两层之间的通信。注意:也可能有特定于域的域服务,与特定于应用程序的应用程序服务分开

[e]
数据库
类根本不需要!您已经拥有了非常优雅和强大的PDO,可以处理数据库操作

[f]实际上,“调用调用PDO方法的方法”并没有错。此链中的每个方法都封装了特定于当前对象的特定行为。不过,每个方法的功能都应该增加一些附加值。否则,拥有这条链条就毫无意义了。例如:在应用程序服务中,您可以直接使用数据映射器从数据库中按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/
的适当类实现

注意
SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php
中的方法
updateBook()。我在其中包含了一个事务代码,以及两个很棒的链接。玩得开心

项目结构:

SampleMvc/App/Controller/Book/AddBook.php:


一些建议:

[a]您不应该在类方法内部创建对象(使用“new”)。相反,您应该将现有实例注入构造函数/设置器。这被命名为依赖项注入,可以与依赖项注入容器一起应用

[b]正如@YourCommonSense所指出的,
数据库将从注入构造函数的单个PDO实例中获益匪浅。注入任务将是DI容器的工作。例如,如果要使用,将有一个用于创建数据库连接的

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;
    },
];
还有一个<?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; } }
<?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;
    }

}
<?php

namespace SampleMvc\App\Service\Book\Exception;

/**
 * An exception thrown if a book already exists.
 */
class BookAlreadyExists extends \OverflowException {
    
}
<?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);
    }

}
<?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);
    }

}
<?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;
    }

}
<?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;
    }

}
<?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;
    }

}
<?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;
    }

}
<?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;
    }

}
<?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;
}