4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHP/Docker/Twig】初めて生PHPのアプリケーション作成してみた【Pスク生必見!?】

Last updated at Posted at 2024-09-06

はじめに

初めまして。Ricccckです。

私は、ReactやJavaScriptには、以前触れたことがあるのですが、今回は、初めてのバックエンド言語。初めてPHPに触れ、わからないこと、難しいことも多くありました...。

そんな中で、初めてアプリケーションを作成したので、今回は、その備忘録を書き留めておこうと思います。

この記事では、コードの詳しい内容までは触れず、「全体のモデルの話」や「コードの書き方」に注目して記事をまとめていきます。DockerとPHPの連携、GDライブラリの使用などで詰まった点もあったので、他の初学者の方に、少しでも参考になればと思います!

今は、twigを用いたフロント実装はあまりないかとも思いますが、Laravelではない 「生のPHP」 を触る機会がある場合には、うってつけの教材になると思いますので、ぜひ触れてみてください。

以下敬略にて失礼します。

ビルドイメージ

ロゴ画像

トップ画像

スクリーンショット 2024-08-29 23.08.11.png

機能

  • 画像一覧
  • 画像検索
  • ユーザー登録(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/mysqldataフォルダを作成し、
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のデータ),(...) テーブルにデータを挿入する際に使用するコマンド。,で区切ることにより複数のデータを同時に挿入することが可能

sqlファイルは理解もだが、テーブルの作成や改善に時間が取られた。 Schemaの作成を先に行い、テーブルを作成する癖をつけていきたい。

5. Dockerの起動

docker-compose buildコマンドを実行し、コンテナを作成スクリーンショット 2024-08-30 11.03.31.png
スクリーンショット 2024-08-30 11.04.21.png
問題なく実行できていれば上記のようになる。

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は、MaterialUIshadcnといった、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で、このアプリケーションを作成していきたいと思います。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?