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

zc0qhyus  于 2023-03-28  发布在  PHP
关注(0)|答案(1)|浏览(140)

有人可以帮助我吗?我有以下类(所有功能,为了易读性在这里缩写):

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

我知道我只能访问Database类内部的PDO beginTransaction,但是否有其他方法,或者我必须使用这个复杂的路径,调用调用PDO方法的方法?
我感觉我在做一些非常愚蠢的事情,也许是将BookModel扩展到Database类,但这感觉也不对。
谢谢!

aydmsdu9

aydmsdu91#

一些建议:
[a]不应该在类方法内部创建对象(带 “new”),而应该将已有的示例注入到构造函数/setter中。这被称为依赖注入,可以通过依赖注入容器来应用。

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;
    },
];

另一个definition条目将其注入Database

return [
    Database::class => autowire()
        ->constructorParameter('connection', get('database-connection')),
];

Database构造器看起来像:

public function __construct(PDO $connection) {
    $this->dbh = $connection;
}

[c]模型不是类(如BookModel),而是一个层(模型层,或domain model),由多个组件组成:实体(或域对象**)、value objectsdata mappersrepositories域服务。您的BookModel至少是实体和数据Map器的组合。注意:从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;
    }

}

现在,您可以使用repository来隐藏查询数据来自数据库的事实。(这里是Book),因此,其他组件认为存储库是书籍的集合,而不是某个数据库中的一堆数据,然后他们向仓库请求相应的数据。仓库将依次询问数据Map器以查询数据库。因此,前面的代码变为:

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/Model/接口注入来自SampleMvc/Domain/Infrastructure/的适当类实现。

请注意SampleMvc/Domain/Infrastructure/Book/PdoBookMapper.php中的方法updateBook()。我在其中包含了一个交易代码,沿着两个很棒的链接。祝您玩得开心。

项目结构:

让我们假设以下路由的定义(可能在“routes.php”文件中找到):

<?php

// Adds/updates a book record in the database.
$routeCollection->post('/books/add', [
    'controller' => SampleMvc\App\Controller\Book\AddBook::class,
    'view' => SampleMvc\App\View\Book\AddBook::class,
]);

// Finds books by author name.
$routeCollection->get('/books/find/{authorName}', [SampleMvc\App\View\Book\FindBooks::class, 'findBooksByAuthorName']);

应用程序的所有对象都应该由依赖注入容器创建。
但是,如果没有它,创建和调用控制器和/或视图的代码可能看起来像这样:

执行“POST”请求添加图书时,文件“index.php”:

<?php

use SampleMvc\App\{
    Controller\Book\AddBook as AddBookController,
    View\Book\AddBook as AddBookView,
};

// [...]

// Controller needed for updating the domain model.
// View needed for querying the domain model and bulding the response.
$addBookView = new AddBookView($responseFactory, $templateRenderer);
$addBookController = new AddBookController($addBookView, $addBookService);

$addBookController($request);
$response = $addBookView();

// Present the response to the user.
$responseEmitter->emit($response);

文件“index.php”以防执行“GET”请求查找图书列表:

<?php

use SampleMvc\App\View\Book\FindBooks as FindBooksView;

// [...]

// No controller needed for querying the domain model !!!
// Only view needed for querying the domain model and building the response.
$findBooksView = new FindBooksView($findBooksService, $responseFactory, $templateRenderer);

$response = $findBooksView->findBooksByAuthorName($authorName);

// Present the response to the user.
$responseEmitter->emit($response);

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

<?php

namespace SampleMvc\App\Controller\Book;

use SampleMvc\App\Service\Book\{
    AddBook as AddBookService,
    Exception\BookAlreadyExists,
};
use Psr\Http\Message\ServerRequestInterface;
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', [
 *  'controller' => SampleMvc\App\Controller\Book\AddBook::class,
 *  'view' => SampleMvc\App\View\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 void
     */
    public function __invoke(ServerRequestInterface $request): void {
        $authorName = $request->getParsedBody()['authorName'];
        $title = $request->getParsedBody()['title'];

        try {
            $this->addBookService($authorName, $title);
        } catch (BookAlreadyExists $exception) {
            $this->view->setErrorMessage(
                $exception->getMessage()
            );
        }
    }

}

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/App/View/Book/AddBook.php:

<?php

namespace SampleMvc\App\View\Book;

use SampleMvc\App\View\View;
use Psr\Http\Message\ResponseInterface;

/**
 * A view for adding a book.
 * Let's assume the existence of this route definition:
 * 
 * $routeCollection->post('/books/add', [
 *  'controller' => SampleMvc\App\Controller\Book\AddBook::class,
 *  'view' => SampleMvc\App\View\Book\AddBook::class,
 * ]);
 */
class AddBook extends View {

    /**
     * Add a book.
     * 
     * @return ResponseInterface The response to the current request.
     */
    public function _invoke(): ResponseInterface {
        $bodyContent = $this->templateRenderer->render('@Templates/Book/AddBook.html.twig', [
            'error' => $this->errorMessage,
        ]);

        $response = $this->responseFactory->createResponse();
        $response->getBody()->write($bodyContent);

        return $response;
    }

}

SampleMvc/App/View/Book/FindBooks.php:

<?php

namespace SampleMvc\App\View\Book;

use SampleMvc\App\{
    View\View,
    Service\Book\FindBooks as FindBooksService,
};
use Psr\Http\Message\{
    ResponseInterface,
    ResponseFactoryInterface,
};
use SampleLib\Template\Renderer\TemplateRendererInterface;

/**
 * A view for finding books.
 * Let's assume the existence of this route definition:
 * 
 * $routeCollection->get('/books/find/{authorName}', [SampleMvc\App\View\Book\FindBooks::class, 'findBooksByAuthorName']);
 */
class FindBooks extends View {

    /**
     * @param FindBooksService $findBooksService An application service for finding books by querying the model layer.
     */
    public function __construct(
        private FindBooksService $findBooksService,
        ResponseFactoryInterface $responseFactory,
        TemplateRendererInterface $templateRenderer,
    ) {
        parent::__construct($responseFactory, $templateRenderer);
    }

    /**
     * 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);

        $bodyContent = $this->templateRenderer->render('@Templates/Book/FindBooks.html.twig', [
            'books' => $books,
        ]);

        $response = $this->responseFactory->createResponse();
        $response->getBody()->write($bodyContent);

        return $response;
    }

}

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 void
     */
    public function __invoke(?string $authorName, ?string $title): void {
        $book = $this->createBook($authorName, $title);

        $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 void
     * @throws BookAlreadyExists The book already exists.
     */
    private function storeBook(Book $book): void {
        if ($this->bookMapper->bookExists($book)) {
            throw new BookAlreadyExists(
                    'A book with the author name "' . $book->getAuthorName() . '" '
                    . 'and the title "' . $book->getTitle() . '" already exists'
            );
        }

        $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/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/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;
}

相关问题