Подскажите по архитектуре моего кода, по солиду, слоям, что улучшить?
Пробую писать в парадигме слоистых архитектур, или чистых, как у дяди Боба.
Сделал на yii2 игру крестики нолики. Можете подсказать, все ли делаю так как надо? По солид, по слоям?
Вот код:
<?php
declare(strict_types=1);
namespace app\modules\crosses\controllers;
use yii\web\Controller;
use app\modules\crosses\models\ARGame;
use Yii;
use app\modules\crosses\useCases\GameUser;
use yii\web\NotFoundHttpException;
use app\modules\crosses\repository\GameRepository;
use yii\data\ActiveDataProvider;
/**
* Default controller for the `crosses` module
*/
class DefaultController extends Controller
{
private $gameUser;
private $repository;
public function __construct($id, $module, GameUser $players, GameRepository $repository, $config = [])
{
parent::__construct($id, $module, $config);
$this->gameUser = $players;
$this->repository = $repository;
}
/**
* Renders the index view for the module
* @return string
*/
public function actionIndex()
{
return $this->render('index');
}
public function actionAllGames()
{
$query = ARGame::find();
$provider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 5,
],
'sort' => [
'defaultOrder' => [
'id' => SORT_DESC,
]
],
]);
return $this->render('all-games', ['provider' => $provider]);
}
public function actionStartGame()
{
$request = Yii::$app->request;
if ($request->post('start_game') == 'yes') {
try {
$id = $this->gameUser->startNewGame();
Yii::$app->session->setFlash('success', 'Игра успешно создана, начинаем играть');
return $this->redirect(['play-game', 'id' => $id]);
} catch (\Throwable $e) {
Yii::error('unable to create a game, because - ' . $e->getMessage());
Yii::$app->session->setFlash('error', $e->getMessage());
}
}
return $this->render('start-game');
}
public function actionPlayGame(int $id)
{
$arGame = $this->findGame($id);
$isOver = $arGame->getIsOver();
$game = $this->repository->getGame($arGame);
$request = Yii::$app->request;
if (!$isOver && ($move = $request->post('make_move'))) {
try {
$inputs = explode('_', $move);
$inputs = array_map('intval', $inputs);
if (count($inputs) != 2) {
throw new \LogicException('invalid move input');
}
$this->gameUser->playGame($id, $game, $inputs[0], $inputs[1]);
} catch (\Throwable $e) {
Yii::error('unable to save a game, because - ' . $e->getMessage());
Yii::$app->session->setFlash('error', $e->getMessage());
}
}
return $this->render('play-game', ['game' => $game]);
}
protected function findGame(int $id)
{
if ($game = ARGame::findOne($id)) {
return $game;
}
throw new NotFoundHttpException();
}
}
?>
<?php
declare(strict_types=1);
namespace app\modules\crosses\models;
use Yii;
/**
* Active Record модель, ассоциированная с сохраненной в базе игрой
*
* @property int $id
* @property int|null $status
* @property string|null $board
* @property string|null $player
* @property string|null $winner
*/
class ARGame extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'crosses';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['status'], 'integer'],
[['board'], 'string'],
[['winner', 'player'], 'string', 'max' => 1],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'status' => 'Status',
'board' => 'Board',
'winner' => 'Winner',
];
}
public function getIsOver(): bool
{
return $this->status == Game::STATUS_GAME_OVER;
}
}
?>
<?php
declare(strict_types=1);
namespace app\modules\crosses\models;
/**
* Доска или поле для игры
*
* @package app\modules\crosses\models
*/
class Board
{
/**
* @var array Массив 3 на 3 с данными о доске.
* Пустая строка, значит клетка не занята.
*/
private $data = [];
public function __construct($data = null)
{
if ($data) {
$this->data = $data;
} else {
$this->setEmpty();
}
}
public function getData() : array
{
return $this->data;
}
public function occupyCell(string $who, int $x, int $y)
{
if ($this->data[$x][$y]) {
throw new \LogicException('Wrong input for move, cell is already taken');
}
$this->data[$x][$y] = $who;
}
/**
* Проверяем , победил ли кто то. Диагонали не проверяем пока
* @return bool|mixed
*/
public function getWinnerIfThereIs()
{
$data = $this->data;
for ($i = 0; $i < 3; $i++)
{
if ($res = $this->checkRow($i)) {
return $res;
}
if ($res = $this->checkColumn($i)) {
return $res;
}
}
return false;
}
protected function checkRow($i)
{
$data = $this->data;
if (!($k = $data[$i][0])) {
return false;
}
for ($j = 1; $j < 3; $j++)
{
if ($k !== $data[$i][$j]) {
return false;
}
}
return $k;
}
protected function checkColumn($i)
{
$data = $this->data;
if (!($k = $data[0][$i])) {
return false;
}
for ($j = 1; $j < 3; $j++)
{
if ($k !== $data[$j][$i]) {
return false;
}
}
return $k;
}
public function getHasEmptyCells()
{
$data = $this->data;
for ($i = 0; $i < 3; $i++)
{
for ($j = 0; $j < 3; $j++)
{
if (!$data[$i][$j]) {
return true;
}
}
}
return false;
}
private function setEmpty()
{
$this->data = [
['', '', ''],
['', '', ''],
['', '', ''],
];
}
}
?>
<?php
declare(strict_types=1);
namespace app\modules\crosses\models;
/**
* Игра
* @package app\modules\crosses\models
*/
class Game
{
const STATUS_GAME_PLAY = 0;
const STATUS_GAME_OVER = 1;
const PLAYER_CROSS = 'x';
const PLAYER_NAUGHT = 'o';
private int $status = self::STATUS_GAME_PLAY;
private $player;
private $board;
private $hasWinner = false;
public function __construct($status, Board $board, Player $player, $hasWinner = false)
{
$this->status = $status;
$this->board = $board;
$this->player = $player;
$this->hasWinner = $hasWinner;
}
public function getStatus() : int
{
return $this->status;
}
public function getBoard() : Board
{
return $this->board;
}
public function getPlayer() : Player
{
return $this->player;
}
public function getIsOver() : bool
{
return $this->status == self::STATUS_GAME_OVER;
}
public function setIsOver(bool $hasWinner): void
{
$this->hasWinner = $hasWinner;
$this->status = self::STATUS_GAME_OVER;
}
public function getHasWinner(): bool
{
return $this->hasWinner;
}
public function swapPlayers() :void
{
$this->player = new Player($this->getPlayer()->getName() == self::PLAYER_CROSS ? self::PLAYER_NAUGHT : self::PLAYER_CROSS);
}
public function playerMakeMove(int $x, int $y)
{
$this->board->occupyCell($this->player->getName(), $x, $y);
}
public static function getRandomPlayerName() : string
{
return mt_rand(0, 1) == 1 ? self::PLAYER_CROSS : self::PLAYER_NAUGHT;
}
}
?>
<?php
declare(strict_types=1);
namespace app\modules\crosses\models;
/**
* Текущий Игрок
* @package app\modules\crosses\models
*/
class Player
{
private $who;
public function __construct($who)
{
$this->who = $who;
}
public function getName()
{
return $this->who;
}
}
?>
<?php
declare(strict_types=1);
namespace app\modules\crosses\repository;
use app\modules\crosses\models\ARGame;
use app\modules\crosses\models\Game;
use app\modules\crosses\models\Board;
use app\modules\crosses\models\Player;
/**
* Репозиторий
*/
class GameRepository
{
public function saveNewGame(Game $game) : int
{
$ar = new ARGame();
$ar->status = $game->getStatus();
$ar->board = serialize($game->getBoard()->getData());
$ar->player = $game->getPlayer()->getName();
if (!$ar->save()) {
throw new \DomainException('Unable to save a new game');
}
return $ar->id;
}
public function saveGame($id, Game $game) : int
{
$ar = ARGame::findOne($id);
if (!$ar) {
throw new \LogicException('Game not found');
}
$ar->status = $game->getStatus();
$ar->board = serialize($game->getBoard()->getData());
$ar->player = $game->getPlayer()->getName();
if ($game->getIsOver() && $game->getHasWinner()) {
$ar->winner = $ar->player;
}
if (!$ar->save()) {
throw new \DomainException('Unable to save a game');
}
return $ar->id;
}
public function getGame(ARGame $ar) : Game
{
$board = new Board(unserialize($ar->board));
$player = new Player($ar->player);
return new Game(intval($ar->status), $board,$player, boolval($ar->winner));
}
}
?>
<?php
declare(strict_types=1);
namespace app\modules\crosses\useCases;
use app\modules\crosses\models\{Board,Game,Player};
use app\modules\crosses\repository\GameRepository;
class GameUser
{
public function startNewGame() : int
{
$board = new Board();
$player = new Player(Game::getRandomPlayerName());
$game = new Game(Game::STATUS_GAME_PLAY, $board,$player);
$repository = new GameRepository();
$id = $repository->saveNewGame($game);
return $id;
}
public function playGame(int $idGame, Game $game, int $x, int $y)
{
$board = $game->getBoard();
$game->playerMakeMove($x, $y);
if ($res = $board->getWinnerIfThereIs()) {
$game->setIsOver(true);
} else {
if ($board->getHasEmptyCells()) {
$game->swapPlayers();
} else {
$game->setIsOver(false);
}
}
$repository = new GameRepository();
$repository->saveGame($idGame, $game);
}
}
?>
<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/** @var yii\web\View $this */
/** @var app\modules\crosses\models\Game $game */
/** @var yii\widgets\ActiveForm $form */
$who = $game->getPlayer()->getName();
$board = $game->getBoard()->getData();
?>
<div class="article-form">
<?php $form = ActiveForm::begin(); ?>
<p>
<?php
$message = '';
if ($game->getIsOver()) {
$message .= 'Игра завершена.';
$message .= !$game->getHasWinner() ? ' Без Победителя.' : ' Победил - ' . '<strong>' . $who . '</strong>';
} else {
$message .= 'Текущий ход - игрок ' . '<strong>' . $who . '</strong>';
}
print $message;
?>
</p>
<table class="table table-bordered">
<?php
for ($i = 0; $i < 3; $i++) {
print '<tr>';
for ($j = 0; $j < 3; $j++) {
$cell = $board[$i][$j];
$content = $cell ? $cell :
($game->getIsOver() ? '--' : Html::submitButton(' ', ['class' => 'btn btn-success', 'name' => 'make_move', 'value' => $i . '_' . $j]));
print '<td>' . $content . '</td>';
}
print '</tr>';
}
?>
</table>
<?php ActiveForm::end(); ?>
</div>