mvc 网站中的静态和非静态方法

static and non static methods in mvc websites

目前我遇到一个问题,它不允许我在不执行任何类型的意大利面条代码的情况下继续向我的 mvc 网站添加功能。

我有两个classes,一个是ModModel,另一个是ModUploadModel。两者都使用模型 class 进行了扩展。 ModModel 包含所有关于“mods”的方法,如 ModModel->doesModNameExists(), ModModel->getModDetails() 等... ModUploadModel 包含了一个 mod 上传的所有方法,如 ModUploadModel->upload(), ModUploadModel->isModNameValid() 等...

在某些情况下,我必须从 ModUploadModel 调用一些 ModModel 方法,为此我必须在 ModUploadController 中创建一个新的 ModModel 实例,并将其作为参数传递给 ModUploadModel->upload()。 例如:ModUploadController 创建两个新对象,$modModel = new ModModel() 和 $modUploadModel = new ModUploadModel(),然后调用 $modUploadModel->upload($ mod型号).

这是 ModUploadController,它创建两个对象并调用 ModUploadModel->upload() 方法

class ModUploadController extends Mvc\Controller {

    public function uploadMod(): void {
        $modUploadModel = new ModUploadModel()
        $modModel = new ModModel();

        // $modModel needs to be passed because the ModUploadModel needs
        // one of its methods
        if ($modUploadModel->upload("beatiful-mod", $modModel)) {
            // success
        } else {
            // failure
        }
    }
}

ModUploadModel->upload() 检查输入是否有效(如果 mod 名称尚未被使用等),最后将 mod 数据上传到数据库中。显然,它在更多的子私有方法中都是 suddivise,如 ModUploadModel->isModNameValid() 和 ModUploadModel->insertIntoDb()。

问题是我没有使用所有静态方法构建我的 classes,每次我必须将对象作为参数传递,就像 ModModel 一样(例如,我需要它的 isModNameValid() 方法) . 我考虑过将所有 ModModel 方法设为静态,但这并不像看起来那么简单,因为它的所有方法都查询数据库,并且它们使用 Model->executeStmt() 方法(记住所有 FooBarModel classes使用模型 class 进行扩展,其中包含有用的常用方法,如 executeStmt() 和其他方法),并且从静态方法调用非静态方法在 php 中不是一个好的做法,所以我应该模型方法也是静态的,因此数据库连接的 Dbh 方法也是静态的(模型是用 Dbh 扩展的)。

ModModel class:

class ModModel extends Mvc\Model {

    // in reality it queries the db with $this->executeStmt(),
    // which is a Model method
    public function doesModNameExists($name) {
        if (/* exists */) {
            return true;
        }

        return false;
    }

}

ModUploadModel class:

class ModUploadModel extends Mvc\Model {

    private $modName;

    public function upload($modName, $modModel) {
        $this->modName = $modName;

        if (!$this->isModNameValid($modModel)) {
            return false;
        }
    
        if ($this->insertIntoDb()) {
            return true;
        }
    
        return false;
    }
    
    // this methods needs to use the non static doesModNameExists() method
    // which is owned by the ModModel class, so i need to pass
    // the object as an argument
    private function isModNameValid($modModel) {
        if ($modModel->doesModNameExists($this->modName)) {
            return false;
        }
        
        // other if statements

        return true;
    }
    
    private function insertIntoDb() {
        $sql = "INSERT INTO blabla (x, y) VALUES (?, ?)";
        $params = [$this->modName, "xxx"];
    
        if ($this->executeStmt($sql, $params)) {
            return true;
        }
    
        return false;
    }
}

另一种方法是在 ModModel 方法中创建一个新的 Model 实例,例如 (new Model)->executeStmt()。问题是创建新对象不是 model 的工作,而且通常这不是我最喜欢的解决方案。

一些观察和建议:

[a] 您正在将 ModModel 对象传递给 ModUploadModel 以在上传前验证 mod 名称。如果具有所提供名称的 mod 已经存在,您甚至不应该尝试调用 ModUploadModel::upload()。所以你应该按照类似这样的步骤操作:

class ModUploadController extends Mvc\Controller {

    public function uploadMod(): void {
        $modUploadModel = new ModUploadModel()
        $modModel = new ModModel();

        $modName = 'beatiful-mod';

        try {
            if  ($modModel->doesModNameExists($modName)) {
                throw new \ModNameExistsException('A mod with the name "' . $modName . '" already exists');
            }

            $modUploadModel->upload($modName);
        } catch (\ModNameExistsException $exception){
            // ...Present the exception message to the user. Use $exception->getMessage() to get it...
        }
    }
}

[b] 在 class 中创建对象是个坏主意(比如在 ModUploadController 中)。使用 dependency injection instead. Read this and watch this and this。所以解决方案看起来像这样:

class ModUploadController extends Mvc\Controller {

    public function uploadMod(ModUploadModel $modUploadModel, ModModel $modModel): void {
        //... Use the injected objects ($modUploadModel and $modModel ) ...
    }
}

在一个项目中,所有需要注入的对象都可以通过“依赖注入容器”创建。例如,PHP-DI (which I recommend), or other DI containers。因此,DI 容器负责项目的所有依赖项注入。例如,在您的情况下,注入 ModUploadController::uploadMod 方法的两个对象将由 PHP-DI 自动创建。您只需在用作应用程序入口点的文件中编写三行代码,可能 index.php:

use DI\ContainerBuilder;

$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true);
$container = $containerBuilder->build();

当然,DI容器也需要配置步骤。但是,在几个小时内,您就可以了解如何以及在何处进行操作。

通过使用 DI 容器,您将能够专注于项目的逻辑,而不是如何以及在何处创建各种组件,或类似的任务。

[c] 使用静态方法是个坏主意。我的建议是摆脱您已经编写的所有静态方法。观看 this, read this, this and this。因此,您遇到的注入问题的解决方案就是上面的那个:DI,由 DI 容器执行。根本不创建静态方法。

[d] 您正在使用这两个组件来查询数据库(ModModeldoesModNameExists()ModUploadModelinsertIntoDb()).您应该只使用一个组件来处理数据库。

[e]你根本不需要Mvc\Model

[f]你根本不需要Mvc\Controller

一些代码:

我写了一些代码,作为你的替代品(我从中以某种方式“推断”了任务)。也许它会对你有所帮助,看看别人会如何编码。它会让您有可能 “在我的 mvc 网站上添加功能,而无需执行任何类型的意大利面条代码”。该代码与我不久前写的 中的代码非常相似。该答案还包含其他重要建议和资源。

重要提示:请注意应用程序服务,例如来自 Mvc/App/Service/ 的所有组件应该 ONLY 与域 model 组件通信,例如使用来自 Mvc/Domain/Model/ 的组件(主要是接口),而不是来自 Mvc/Domain/Infrastructure/。反过来,您选择的 DI 容器将负责从 Mvc/Domain/Infrastructure/ 为应用程序服务使用的 Mvc/Domain/Model/ 接口注入正确的 class 实现。

注意:我的代码使用 PHP 8.0。祝你好运。

项目结构:

Mvc/App/Controller/Mod/AddMod.php:

<?php

namespace Mvc\App\Controller\Mod;

use Psr\Http\Message\{
    ResponseInterface,
    ServerRequestInterface,
};
use Mvc\App\Service\Mod\{
    AddMod As AddModService,
    Exception\ModAlreadyExists,
};
use Mvc\App\View\Mod\AddMod as AddModView;

class AddMod {

    /**
     * @param AddModView $addModView A view for presenting the response to the request back to the user.
     * @param AddModService $addModService An application service for adding a mod to the model layer.
     */
    public function __construct(
        private AddModView $addModView,
        private AddModService $addModService,
    ) {
        
    }

    /**
     * Add a mod.
     * 
     * The mod 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 addMod(ServerRequestInterface $request): ResponseInterface {
        // Read the values submitted by the user.
        $name = $request->getParsedBody()['name'];
        $description = $request->getParsedBody()['description'];

        // Add the mod.
        try {
            $mod = $this->addModService->addMod($name, $description);
            $this->addModView->setMod($mod);
        } catch (ModAlreadyExists $exception) {
            $this->addModView->setErrorMessage(
                $exception->getMessage()
            );
        }

        // Present the results to the user.
        $response = $this->addModView->addMod();

        return $response;
    }

}

Mvc/App/Service/Mod/Exception/ModAlreadyExists.php:

<?php

namespace Mvc\App\Service\Mod\Exception;

/**
 * An exception thrown if a mod already exists.
 */
class ModAlreadyExists extends \OverflowException {
    
}

Mvc/App/Service/Mod/AddMod.php:

<?php

namespace Mvc\App\Service\Mod;

use Mvc\Domain\Model\Mod\{
    Mod,
    ModMapper,
};
use Mvc\App\Service\Mod\Exception\ModAlreadyExists;

/**
 * An application service for adding a mod.
 */
class AddMod {

    /**
     * @param ModMapper $modMapper A data mapper for transfering mods 
     * to and from a persistence system.
     */
    public function __construct(
        private ModMapper $modMapper
    ) {
        
    }

    /**
     * Add a mod.
     * 
     * @param string|null $name A mod name.
     * @param string|null $description A mod description.
     * @return Mod The added mod.
     */
    public function addMod(?string $name, ?string $description): Mod {
        $mod = $this->createMod($name, $description);

        return $this->storeMod($mod);
    }

    /**
     * Create a mod.
     * 
     * @param string|null $name A mod name.
     * @param string|null $description A mod description.
     * @return Mod The newly created mod.
     */
    private function createMod(?string $name, ?string $description): Mod {
        return new Mod($name, $description);
    }

    /**
     * Store a mod.
     * 
     * @param Mod $mod A mod.
     * @return Mod The stored mod.
     * @throws ModAlreadyExists The mod already exists.
     */
    private function storeMod(Mod $mod): Mod {
        if ($this->modMapper->modExists($mod)) {
            throw new ModAlreadyExists(
                    'A mod with the name "' . $mod->getName() . '" already exists'
            );
        }

        return $this->modMapper->saveMod($mod);
    }

}

Mvc/App/View/Mod/AddMod.php:

<?php

namespace Mvc\App\View\Mod;

use Mvc\{
    App\View\View,
    Domain\Model\Mod\Mod,
};
use Psr\Http\Message\ResponseInterface;

/**
 * A view for adding a mod.
 */
class AddMod extends View {

    /** @var Mod A mod. */
    private Mod $mod = null;

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

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

        return $response;
    }

    /**
     * Set the mod.
     * 
     * @param Mod $mod A mod.
     * @return static
     */
    public function setMod(Mod $mod): static {
        $this->mod = $mod;
        return $this;
    }

}

Mvc/App/View/View.php:

<?php

namespace Mvc\App\View;

use Psr\Http\Message\ResponseFactoryInterface;
use SampleLib\Template\Renderer\TemplateRendererInterface;

/**
 * A view.
 */
abstract class View {

    /** @var string An error message */
    protected string $errorMessage = '';

    /**
     * @param ResponseFactoryInterface $responseFactory A response factory.
     * @param TemplateRendererInterface $templateRenderer A 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;
    }

}

Mvc/Domain/Infrastructure/Mod/PdoModMapper.php:

<?php

namespace Mvc\Domain\Infrastructure\Mod;

use Mvc\Domain\Model\Mod\{
    Mod,
    ModMapper,
};
use PDO;

/**
 * A data mapper for transfering Mod entities to and from a database.
 * 
 * This class uses a PDO instance as database connection.
 */
class PdoModMapper implements ModMapper {

    /**
     * @param PDO $connection Database connection.
     */
    public function __construct(
        private PDO $connection
    ) {
        
    }

    /**
     * @inheritDoc
     */
    public function modExists(Mod $mod): bool {
        $sql = 'SELECT COUNT(*) as cnt FROM mods WHERE name = :name';

        $statement = $this->connection->prepare($sql);
        $statement->execute([
            ':name' => $mod->getName(),
        ]);

        $data = $statement->fetch(PDO::FETCH_ASSOC);

        return ($data['cnt'] > 0) ? true : false;
    }

    /**
     * @inheritDoc
     */
    public function saveMod(Mod $mod): Mod {
        if (isset($mod->getId())) {
            return $this->updateMod($mod);
        }
        return $this->insertMod($mod);
    }

    /**
     * Update a mod.
     * 
     * @param Mod $mod A mod.
     * @return Mod The mod.
     */
    private function updateMod(Mod $mod): Mod {
        $sql = 'UPDATE mods 
                SET 
                    name = :name,
                    description = :description 
                WHERE 
                    id = :id';

        $statement = $this->connection->prepare($sql);
        $statement->execute([
            ':name' => $mod->getName(),
            ':description' => $mod->getDescription(),
        ]);

        return $mod;
    }

    /**
     * Insert a mod.
     * 
     * @param Mod $mod A mod.
     * @return Mod The newly inserted mod.
     */
    private function insertMod(Mod $mod): Mod {
        $sql = 'INSERT INTO mods (
                    name,
                    description
                ) VALUES (
                    :name,
                    :description
                )';

        $statement = $this->connection->prepare($sql);
        $statement->execute([
            ':name' => $mod->getName(),
            ':description' => $mod->getDescription(),
        ]);

        $mod->setId(
            $this->connection->lastInsertId()
        );

        return $mod;
    }

}

Mvc/Domain/Model/Mod/Mod.php:

<?php

namespace Mvc\Domain\Model\Mod;

/**
 * Mod entity.
 */
class Mod {

    /**
     * @param string|null $name (optional) A name.
     * @param string|null $description (optional) A description.
     */
    public function __construct(
        private ?string $name = null,
        private ?string $description = 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 name.
     * 
     * @return string|null
     */
    public function getName(): ?string {
        return $this->name;
    }

    /**
     * Set the name.
     * 
     * @param string|null $name A name.
     * @return static
     */
    public function setName(?string $name): static {
        $this->name = $name;
        return $this;
    }

    /**
     * Get the description.
     * 
     * @return string|null
     */
    public function getDescription(): ?string {
        return $this->description;
    }

    /**
     * Set the description.
     * 
     * @param string|null $description A description.
     * @return static
     */
    public function setDescription(?string $description): static {
        $this->description = $description;
        return $this;
    }

}

Mvc/Domain/Model/Mod/ModMapper.php:

<?php

namespace Mvc\Domain\Model\Mod;

use Mvc\Domain\Model\Mod\Mod;

/**
 * An interface for various data mappers used to 
 * transfer Mod entities to and from a persistence system.
 */
interface ModMapper {

    /**
     * Check if a mod exists.
     * 
     * @param Mod $mod A mod.
     * @return bool True if the mod exists, false otherwise.
     */
    public function modExists(Mod $mod): bool;

    /**
     * Save a mod.
     * 
     * @param Mod $mod A mod.
     * @return Mod The saved mod.
     */
    public function saveMod(Mod $mod): Mod;
}