はじめに
初めまして。Ricccckです。
私は、ReactやJavaScriptには、以前触れたことがあるのですが、今回は、初めてのバックエンド言語。初めてPHPに触れ、わからないこと、難しいことも多くありました...。
そんな中で、初めてアプリケーションを作成したので、今回は、その備忘録を書き留めておこうと思います。
この記事では、コードの詳しい内容までは触れず、「全体のモデルの話」や「コードの書き方」に注目して記事をまとめていきます。DockerとPHPの連携、GDライブラリの使用などで詰まった点もあったので、他の初学者の方に、少しでも参考になればと思います!
今は、twigを用いたフロント実装はあまりないかとも思いますが、Laravelではない 「生のPHP」 を触る機会がある場合には、うってつけの教材になると思いますので、ぜひ触れてみてください。
以下敬略にて失礼します。
ビルドイメージ
ロゴ画像
トップ画像
機能
- 画像一覧
- 画像検索
- ユーザー登録(Client, Customer)
- ログイン
- ユーザー情報閲覧
- マイページ
- 画像投稿
- 画像購入
- 購入画像ダウンロード
ディレクトリ構成
PhotoStudio
├ docker ┐
│ ├ mysql ┐
│ │ ├ data --- init.sql
│ │ ├--- Dockerfile
│ │ └--- my.cnf
│ ├ nginx ┐
│ │ ├--- default.conf
│ │ └--- Dockerfile
│ └ php ┐
│ ├--- Dockerfile
│ └--- php.ini
├ src -┐
│ ├ app ┐
│ │ ├ css ┐
│ │ │ ├--- (各twigテンプレート用のCSSファイル)
│ │ │ └--- common.css
│ │ ├ fonts --- BlackOpsOne-Regular.ttf
│ │ ├ js ┐
│ │ │ ├--- addTag.js
│ │ │ ├--- changeFavicon.js
│ │ │ ├--- fileDrop.js
│ │ │ └--- sendCartId.js
│ │ ├ logs
│ │ ├ public ┐
│ │ │ ├ sample(写真のサンプルを保存するフォルダ)
│ │ │ ├ upload(アップロードされた写真を保存するフォルダ)
│ │ │ ├--- favicon-black.ico
│ │ │ ├--- favicon-white.ico
│ │ │ ├--- logo-black.png
│ │ │ └--- logo-white.png
│ │ └ (各Modelファイル)
│ ├ libs --- (各Contollerファイル)
│ ├ logs --- photostudio.log
│ ├ templates ┐
│ │ ├ admin --- (admin関係のtwigファイル)
│ │ ├ authentication ---- (ログイン関係のtwigファイル)
│ │ ├ client --- (client関係のtwigファイル)
│ │ ├ common --- (共通のtwigファイル)
│ │ ├ customer --- (customer関係のtwigファイル)
│ │ └--- home.html.twig
│ └- vendor
├--- .env
├--- composer.json
├--- composer.lock
├--- docker-compose.yml
└--- README.md
Docker構築
0. dockerをインストール
Googleで「Docker」と検索するか
以下のURLからDockerをインストールする
1. docker-compose.ymlを作成
PhotoStudio
├--- docker-compose.yml
└--- README.md
docker-compose.yml
version: '3' # dockerのバージョン
services: # 以下に使用するサービスを書いていく
nginx: # 画面表示のサービス
image: nginx:alpine
# 使用するサービスのバージョン
container_name: nginx
# コンテナ名
ports:
# コンテナが使用するポート番号
- "8000:80"
volumes:
# buildするにあたって参照するデータなどのパス
# (docker-compose.ymlからデータまでのパス):(dockerコンテナでのパス)
- ./src:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
# アプリケーションが所属するネットワーク名
- app-network
php: # サービス全般
image: php:8.2.12-fpm
build:
context: .
dockerfile: docker/php/Dockerfile
# phpをビルドする際に参考する設定ファイル(重要)
volumes:
- ./src:/var/www/html
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
networks:
- app-network
mysql: # データベース
image: mysql:8.0
container_name: mysql
ports:
- '3306:3306'
environment: # 環境変数
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
volumes:
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./docker/mysql/data/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-network
phpMyAdmin: # データベース画面管理
image: phpmyadmin/phpmyadmin
container_name: phpmyadmin
environment:
PMA_HOST: mysql
MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
depends_on:
- mysql
ports:
- "3000:80"
networks:
- app-network
networks:
app-network:
driver: bridge
docker-compose.ymlでは、dockerで使用するサービスを定義する
2. Dockerfileファイルを作成
それぞれにDockerfile
を作成する
PhotoStudio
├ docker ┐
│ ├ mysql --- Dockerfile
│ ├ nginx --- Dockerfile
│ └ php --- Dockerfile
├--- docker-compose.yml
└--- README.md
mysql/Dockerfile
FROM mysql:latest
# mysqlのバージョン
ENV TZ=Asia/Tokyo
# タイムゾーン
COPY ./docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf
# mysqlの設定ファイル
COPY ./docker/mysql/data /docker-entrypoint-initdb.d
# 起動時のテーブル読み込み
nginx/Dockerfile
FROM nginx:latest
# nginxのバージョン
ADD ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
# nginxの設定ファイルを追加
ADD ./src /usr/share/nginx/html
# コンテナで表示するソースを追加
RUN echo "start nginx"
# nginxを起動
php/Dockerfile
FROM php:8.2.12-fpm
# phpのバージョン
RUN apt-get update && apt-get install -y \
git \
curl \
zip \
unzip \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo_mysql
# 諸設定とgdライブラリ, pdo_mysqlライブラリの追加
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# composerのインストール
COPY ./src /var/www/html/
COPY ./composer.json /var/www/html/
COPY ./composer.lock /var/www/html/
# コンテナで使用するデータをコピーする
WORKDIR /var/www/html
# 作業ディレクトリの定義
RUN composer install
# composer installの実行
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html
EXPOSE 80
CMD [ "php-fpm" ]
3. 各サービスの設定ファイルを追加
mysql/my.conf
, ngnix/default.conf
, php.ini
を作成する
PhotoStudio
├ docker ┐
│ ├ mysql ┐
│ │ ├--- Dockerfile
│ │ └--- my.cnf
│ ├ nginx ┐
│ │ ├--- default.conf
│ │ └--- Dockerfile
│ └ php ┐
│ ├--- Dockerfile
│ └--- php.ini
├--- docker-compose.yml
└--- README.md
mysql/my.conf
# データベースの文字コードをutf8に
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci
[client]
default-character-set=utf8
ngnix/default.conf
server {
listen 80;
server_name localhost;
client_max_body_size 500M;
root /var/www/html/app;
index home.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index home.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
}
php/php.ini
# 写真共有アプリなのでアップロードできる画像サイズを大きめに
upload_max_filesize = 500M
post_max_size = 500M
memory_limit = 512M
4. mysqlに初期テーブルを作成
docker/mysql
にdata
フォルダを作成し、
dataフォルダ直下にinit.sql
を作成する
PhotoStudio
├ docker ┐
│ ├ mysql ┐
│ │ ├ data --- init.sql
│ │ ├--- Dockerfile
│ │ └--- my.cnf
│ ├ nginx
│ └ php
├--- docker-compose.yml
└--- README.md
init.sql
CREATE TABLE IF NOT EXISTS clients (
client_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
first_name_kana VARCHAR(100) NOT NULL,
last_name_kana VARCHAR(100) NOT NULL,
company_name VARCHAR(100),
email VARCHAR(255) NOT NULL,
phone_number VARCHAR(11) NOT NULL,
sex TINYINT(1) NOT NULL,
zip VARCHAR(8) NOT NULL,
pref VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL,
town VARCHAR(100) NOT NULL,
website VARCHAR(255),
profile_picture VARCHAR(255),
regist_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_deleted TINYINT(1) UNSIGNED NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS client_pass (
client_id INT UNSIGNED PRIMARY KEY,
password_hash VARCHAR(255),
FOREIGN KEY(client_id) REFERENCES clients(client_id) ON DELETE CASCADE
);
--------- 中省略 ---------
-- サンプルデータ
INSERT INTO
clients (
username,
first_name,
last_name,
first_name_kana,
last_name_kana,
company_name,
email,
phone_number,
sex,
zip,
pref,
city,
town,
website,
profile_picture
)
VALUES
(
'Taro106',
'太郎',
'山田',
'タロウ',
'ヤマダ',
'サンプル株式会社',
'client@example.com',
'09012345678',
1,
'1000001',
'東京都',
'千代田区',
'1-1-1',
'http://www.example.com',
'profile1.jpg'
);
--------- 以下省略 ---------
-
CREATE TABLE IF NOT EXISTS テーブル名 ( カラム名 );
テーブルが存在していない場合、テーブルを作成するコマンド -
INT, VARCHER, TINYINT
データ型の宣言 -
NOT NULL
NULLを許容しない -
~_at
DATE型のデータを収納するカラム名は~_at
か~_date
とする -
DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
登録されているデータが更新されるたびに現在の日付で更新する -
is_~
フラグ管理を行う際のカラム名は、is_~, has_~
とするか、~_flg
とする -
FOREIGN KEY(カラム名) REFERENCES 参照するテーブル(参照するカラム名) ON DELETE CASCADE
外部キーで関連付けを行う。ON DELETE CASCADE
を使用することで、参照先が削除された場合、このデータも同時に削除を行うことが可能になる -
INSERT INTO テーブル名 (カラム名1, カラム名2) VALUES (カラム名1のデータ, カラム名2のデータ),(...)
テーブルにデータを挿入する際に使用するコマンド。,
で区切ることにより複数のデータを同時に挿入することが可能
5. Dockerの起動
docker-compose build
コマンドを実行し、コンテナを作成
問題なく実行できていれば上記のようになる。
MVCモデルについて
本アプリケーションでは、MVCモデルを採用している。
MVCモデルとは、Model
, View
, Controller
が相互的に作用するアプリケーションモデルのことだ。従来のコードが1つのファイルに全ての情報を記載していたのに比べ、MVCモデルでは一つのファイルに多くのコードが書かれることを嫌い、簡潔に管理のしやすいアプリケーションができる。
今回のアプリケーションでは
app/~.php
templates/~.html.twig
-
libs/~.class.php
がそれぞれの役割を果たしている。
Model
Modelは、ViewとControllerを繋ぐ役割を担っており、Controllerにどんなデータをデータベースに送って、受け取って、Viewに表示させるのかの処理を書く。
また、できるだけコードを短く保つ必要があり、複雑な処理をする場合もあるが、基本的にはシンプルな構成になっている。
以下は、一例である。
// home.php
<?php
namespace Photostudio;
// このファイルをPhotostudioに割り振り
require_once __DIR__ . '/../lib/Bootstrap.class.php';
// require_onceでBootstrap.class.phpを呼び出し
// autoloadを起動し、残りのControllerをrequire_onceする必要がなくなる
use Photostudio\libs\Bootstrap;
use Photostudio\libs\PDODatabase;
use Photostudio\libs\Photo;
use Photostudio\libs\Client;
use Photostudio\libs\Customer;
// 使用するクラスを読み込む
$db = new PDODatabase(Bootstrap::DB_HOST, Bootstrap::DB_USER, Bootstrap::DB_PASS, Bootstrap::DB_NAME, Bootstrap::DB_TYPE);
$photo = new Photo($db);
$client = new Client($db);
$customer = new Customer($db);
// 使用クラスのインスタンス化
$loader = new \Twig\Loader\FilesystemLoader(Bootstrap::TEMPLATE_DIR);
$twig = new \Twig\Environment($loader, [
'cache' => Bootstrap::CACHE_DIR
]);
// twigの環境設定
$ctg_id = (isset($_GET['ctg_id']) === true && preg_match('/^[0-9]+$/', $_GET['ctg_id']) === 1) ? $_GET['ctg_id'] : '';
// 三項演算子
// 変数 = 条件式 ? 正の場合 : 誤の場合
$ctgArr = $photo->getCategoryList();
$photoArr = $photo->getPhotoList($ctg_id);
$dataArr = [];
if($photoArr !== false){
$randomPhoto = array_rand($photoArr);
$dataArr['random_id'] = $photoArr[$randomPhoto]['photo_id'];
$dataArr['random_url'] = $photoArr[$randomPhoto]['photo_url'];
}
$userArr = [];
if (isset($_SESSION['client'])) {
$userArr = $client->getData($_SESSION['client']);
} elseif (isset($_SESSION['customer'])) {
$userArr = $customer->getData($_SESSION['customer']);
} else {
$userArr['username'] = 'Guest';
}
// ユーザー情報の取得
// インスタンス化された関数からメソッドを使用してデータを取得
$context = [];
$context['ctgArr'] = $ctgArr;
$context['photoArr'] = $photoArr;
$context['dataArr'] = $dataArr;
$context['userArr'] = $userArr;
// twigファイルに送るデータを多次元連想配列で作成
$template = $twig->load('home.html.twig');
// どのtwigファイルをレンダリングするのかを決める
$template->display($context);
// 実際にtwigファイルをレンダリングする
この場合、インスタンス化されたクラスが、Controllerの役割を果たしており、twigがViewの役割を果たしている。
View
Viewは、ModelがContorollerから受け取ったデータをユーザーに表示し、ユーザーが入力したデータをモデルに送信する役割を持つ。
twigは、HTMLにPHPから送信されたデータを使いやすくしたライブラリで、{{}}
と打つことで、どこにでも値を引いてくることができる。
注意点は、モデル内でレンダリングする際に、必要なデータは、必ず渡してやらなければならない点だ。
home.html.twig
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8"/>
<meta
name="viewport" content="width=device-width, initial-scale=1">
{# Bootstrap #}
<link
href="(省略)" crossorigin="anonymous">
{# favicon #}
<link
rel="icon" href="/public/favicon-black.ico"/>
{# javascript #}
<script src="js/changeFavicon.js"></script>
{# css #}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="css/common.css" type="text/css"/>
<link
rel="stylesheet" href="css/home.css" type="text/css"/>
{# title #}
<title>PhotoStudio -ホーム-</title>
</head>
<body>
{% include 'common/header.html.twig' %}
{# includeを使用することで、 別のtwigファイルを読み込ませることが可能 #}
<main
class="contents">
{# photo-search #}
<div id="photo-search" class="mb-3 bg-secondary">
<div id="search-bg-img" style="background-image: url('public/upload/{{dataArr.random_url}}');">
<form method="get" id="search-form" action="list.php">
<input id="search-window" type="search" name="keyword" placeholder="キーワードを入力">
<button id="search-btn">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</form>
</div>
</div>
{# random-photo #}
<div id="random-photo" class="mb-3">
<h1 class="title">
ランダム紹介
</h1>
<div class="photo-list">
{% for value in photoArr %}
<div class="item">
<ul>
<li class="image">
<a href="detail.php?photo_id={{value.photo_id}}">
<img src="public/sample/{{value.sample_url}}" alt="現在利用できません"/>
</a>
</li>
<li class="name">
<a href="detail.php?photo_id={{value.photo_id}}">{{value.photo_title}}</a>
</li>
</ul>
</div>
{% endfor %}
</div>
</div>
{# choice-category #}
<div id="choice-category" class="mb-3">
<h1 class="title">
カテゴリーから探す
</h1>
<div id="category-list">
{% for value in ctgArr %}
<div class="category">
<div class="ctg-bg-img" style="background-image: url('public/ctg_images/{{value.category}}.jpg');">
<div class="ctg-str">
<a href="list.php?ctg_id={{value.category_id}}">
{{value.category}}
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
{% include './common/footer.html.twig' %}
{# Bootstrap #}
<script src="(省略)" crossorigin="anonymous"></script>
</body>
</html>
以上のように、基本的には、HTMLとほぼ変わらない。ところどころで、HTMLタグに埋め込むように、twig変数を使用して画面にデータを表示している。文章中で、forを発生させることができるため、Reactにおける.map
のような使い方をすることができる。
今回はユーザーに見やすくなるように、Bootstrapをかなり後から採用した。そのせいで挙動エラー、表示エラーが死ぬほど出た。
Bootstrap
Bootstrap
は、MaterialUI
やshadcn
といった、UIのHTML版とも言えるようなUIライブラリで、HTMLコードにScriptを埋め込むことで、簡単に実装を行うことができる。もちろん、Reactでも使用可能。
HTMLタグのclass属性にcomponents名を入れ、それぞれのコンポーネントを画面に表示することができる。
CSSほど自由度は効かないが、CSSほど複雑ではないので、ぜひ一度、触れてみてほしい。
Contoroller
Controllerは、ModelがViewから受け取ったデータをデータベースに保存したり、ModelがViewに表示したい情報をデータベースや自身から送る役割を持つ。
機能が増えることから、コードもある程度長くなることが予想されるが、コンパクトなコードが良いことに変わりはないため、常にコードはクリーンに保てるよう努力する必要がある。
今回は、PHPのclassに、Controllerの役割をしてもらった。
Laravelを触る際も、このMVCモデルの動きは出てくるので、頭の中に留めておいてほしい。
以下は一例である。
// Cart.class.php
<?php
namespace Photostudio\lib;
// namespaceをPhotostudio\libに割り振り
class Cart // クラス名
{
private $db = null;
//データをやり取りする際に使う別のクラス用の変数
//privateは外部からアクセスすることができなくなる宣言
public function __construct($db = null) // 初回に関数を呼び出した時に呼び出される関数
{
$this->db = $db;
// private $dbにconstructの引数で受け取ったdbのクラスを入れる
}
public function getCartList($customerId) // 外部から呼び出すことができるメソッドを定義
{
$table = ' cart c JOIN upload_photos up ON c.photo_id = up.photo_id LEFT JOIN price p ON up.price = p.price_id ';
$col = ' crt_id, photo_title, sample_url, p.price ';
$where = ($customerId !== '') ? ' c.customer_id = ? AND is_purchased = ? AND c.is_deleted = ? ' : '';
// どのテーブルのどの行のどの情報を受け取るのか
$arrVal = ($customerId !== '') ? [$customerId, 0, 0] : [];
$res = $this->db->select($table, $col, $where, $arrVal);
// dbのselectメソッドを呼び出して$resを取得
return ($res !== false && count($res) !== 0) ? $res : [];
// 取得したデータに合わせて結果を返す
}
/* ---------- 後略 ----------- */
以上のようなクラス(実際にはメソッドがもっとたくさんある)を作成し、Modelファイルが呼び出すことで、データのやり取りを行う。データベースとのやり取りを行う際には、別のControllerクラスを利用することもある。
最後に
アイデア出しに悩んでいる方へ
「Pスクも、残りわずか...成果物を作れって言われてるけど、何作ればいいんだろうなぁ...」
と、そんなこと思っている、そこの貴方!開発のきっかけは、意外なものだったりしますよ!
私が今回、このアプリケーションにしようと思ったきっかけは「有料写真販売サイトなどで、sampleと文字が書かれた画像は、まさか人間が書いているわけじゃあないでしょ」と思ったからです。
それっぽっちの小さなことが、アプリケーションを作るきっかけになります。
まずは、既存サイトの模倣を行うことで、サイトの開発の仕方を学んでいってください。
模倣ができたら、まずは「プラス1のアイデア」を大切にしていってください。
私の今回のアプリケーションでは、画像サイズで画像の値段を決める点や、ドラッグ&ドロップで画像を追加できるようにするなど、細かい点で、こだわりを入れました。
そうしていくうちに、こんなものが作りたい、という考えがきっと浮かんでくるはずです。
そうなったら最後、培った技術でオリジナルのアイデアを実現していきましょう!
感想
PHPは、初体験だったので、JavaScriptとはルールの異なる点が多く、慣れるまでには、時間を要しました。特に、末尾に必ず;
をつけなくてはならない(VSCodeでも自動的に補完してくれない)のにストレスを感じましたねー。
また、これまではSPAの開発しかやってこなかったので、MPAのページ数の多さに、脱帽しました。 モデルファイル作って、ビューファイル作って、それだけで2枚ずつ増えますから。ページ数は少なめに抑えたつもりですが、コードを書く手間は、倍以上になる気分なので、本当に大変でした。
ここまで読んでいただき、ありがとうございました!
次回は、React+Laravel(API)+Dockerで、このアプリケーションを作成していきたいと思います。