20190728のPHPに関する記事は14件です。

PHPのアロー演算子がわかんなかったのでまとめてみた

アロー演算子とは

言ってしまえばこれのことです。

sample.php
$hoge -> hoge;

この矢印なんやねん!って正直思っているあなた、安心してください私もわかりません。(いいのか?)
ということで、かんたんにまとめておきます。

アロー演算子はクラスのメソッドやプロパティ(変数)にアクセスするための演算子です。
どういうことか、ソースを見たほうが早いでしょう。
そこでオブジェクト指向にありがちな人間というオブジェクトを想定します。

sample.php
<?php

class Human{
    protected $name;
    protected $age;

//説明1
    function __construct($name, $age) {
        $this   ->name  = $name;
        $this   ->age   = $age;
    }

//説明2
    function intro(){
        echo 'My name is '.$this->name.'. My age is '.$this->age.' years old.';
    }
}

$tom = new Human('Tom', '17');

//説明3
$tom->intro();

?>

これを実行すると以下が出力されます。

my name is Tom. My age is 17 years old.

なんで英語やねん、とつっこみたいですがそこはスルー。PHPはおしゃれな言語ですからね(偏見)

説明1

コンストラクタを作っています。見ればわかりますね。
ここで$this->hoge = $hogeが出現します。
これは「ここのhogeは$hogeですよ」といっているわけです。
しかし、$thisってなんやねんという疑問が発生すると思います。これは自分自身だと思ってください。つまりコンストラクタ自身なんです。
コンストラクタ自身のhogeを$hogeとしますという認識で結構だと思います。違ったらご指摘ください。
ここではクラスに$name$ageというプロパティ(変数)がありますね。それにアクセスしているのです。

説明2

これは説明1を読んでいただければわかると思います。
クラス自身、つまり$thisの中にあるnameというプロパティ(変数)やageというプロパティ(変数)を呼び出しているだけです。

説明3

もうここまで来るとわかると思います。
$tomというインスタンスが生成されました、そのインスタンスはHumanクラスから作られたものです。なのでHumanクラスから生成された$tomの中にあるintroというメソッドを呼び出しているだけです。

まとめ

->ってなんやねんと思いましたが、要は左の中にある右のものを取り出しているだけなんですね。私含め、この記事を読んでいる方が少しでも理解していただければ幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPのディレクトリまとめ

PHPのパス、ファイルの取得方法の調査。
$_ENV$_SERVERから取得できるディレクトリパス。
$_SERVER」はWebサーバにより生成されるサーバ情報および実行時の環境情報。
$_ENV」はサーバ側で設定されている環境変数。

dir.php

var_dump($_SERVER['HTTP_HOST']);
var_dump($_SERVER['SERVER_NAME']);
var_dump($_SERVER['DOCUMENT_ROOT']);
var_dump($_SERVER['SCRIPT_FILENAME']);
var_dump($_SERVER['SCRIPT_NAME']);
var_dump($_SERVER['PHP_SELF']);
var_dump($_SERVER['REQUEST_URI']);
var_dump(__FILE__);
var_dump(__DIR__);
var_dump(__LINE__);
var_dump(dirname(__FILE__));
var_dump(dirname(__DIR__));
var_dump(dirname($_SERVER['SCRIPT_NAME']));
var_dump(basename(__FILE__));
var_dump(basename(__DIR__));
var_dump(basename($_SERVER['REQUEST_URI']));

ディレクトリパス取得一覧

ドキュメントルート:/var/www/html
URL:http://example.localhost/dir1/dir2/phpdir.php?test=aaa
にアクセスした場合

変数名 説明
$_SERVER $_SERVER['HTTP_HOST'] example.localhost リクエストヘッダに含まれるHOST
XSSの危険性あり
$_SERVER['SERVER_NAME'] example.localhost Apacheの設定ファイル等に
記述されているSERVER_NAME
$_SERVER['DOCUMENT_ROOT'] /var/www/html ドキュメントルートのフルパス
$_SERVER['SCRIPT_FILENAME'] /var/www/html/dir1/dir2/dir.php フルパスとファイル名
$_SERVER['SCRIPT_NAME'] /dir1/dir2/dir.php 実行されたPHPのパスとファイル名
$_SERVER['PHP_SELF'] /dir1/dir2/dir.php リクエストされたパスとファイル名
XSSの危険性あり
$_SERVER['REQUEST_URI'] /dir1/dir2/dir.php?test=aaa リクエストされた
パスとファイル名とクエリ
XSSの危険性あり
__XXXX__ __FILE__ \var\www\html\dir1\dir2\dir.php 実行されたPHPのフルパス
__DIR__ \var\www\html\dir1\dir2 実行されたPHPのディレクトリ名
dirname(__FILE__)」 と同じ
__LINE__ 12 実行されたPHPのファイル行番号
dirname() dirname(__FILE__) \var\www\html\dir1\dir2 実行されたPHPのディレクトリまでの
フルパス
dirname(__DIR__) \var\www\html\dir1 「実行されたPHPのディレクトリ」
のディレクトリまでのフルパス
dirname($_SERVER['SCRIPT_NAME']) /dir1/dir2 実行されたPHPのディレクトリのパス
dirname($_SERVER['REQUEST_URI']) /dir1/dir2 リクエストされたPHPのパス
XSSの危険性あり
basename() basename(__FILE__) dir.php 実行されたPHPのファイル名
basename(__DIR__) dir2 「実行されたPHPのディレクトリ」
のディレクトリ名
basename($_SERVER['SCRIPT_NAME']) dir.php 実行されたPHPのファイル名
basename($_SERVER['REQUEST_URI']) dir.php?test=aaa リクエストされたファイル名とクエリ
XSSの危険性あり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

php-master-changes 2019-07-27

今日は BOM 付 UTF-8 から BOM を削除する修正があった!

2019-07-27

zebastian: file encoding cleanup: remove bom in win32 files

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ubuntu 18.04にLAMP環境を構築する

ubuntu18.04にLAMP環境を構築したので備忘録。

VirtualBoxのインストール

  • こちらから環境にあったものをインストールする

Ubuntuのインストール

VirtualBoxにUbuntuをインストールする

  • VirtualBoxの画面の新規ボタンを押す
  • 任意の名前をつける
  • メモリを設定する(4GB)
  • 仮装ハードディスクを作成する
  • VDIを選択
  • 可変サイズ(100 GB)
  • okを押す

ネットワークの設定

  • ネットワークの設定
  • 設定→ネットワーク押下
  • アダプター1をホストオンリーアダプターに変更
  • アダプター2をNATに変更
  • ok押下

LAMP環境を構築する

  • ubuntuのターミナルを起動

MySQL インストール

  • mysqlをインストールする
 $ sudo apt-get install mysql-server mysql-client
  • バージョンの確認
$ mysql --version
  • 接続できるか確認する
$ sudo mysql -u root

Apache インストール

  • Apacheをインストールする
$ sudo apt install apache2
  • 設定ファイルの確認
$ sudo apache2ctl configtest
  • 上記実行後、AH00558 と書かれたエラーが発生した場合、設定ファイルとシンボリックリンクを同数にしなければならないため、ファイルを作成する(シンボリックリンクの作成)
$ sudo nano /etc/apache2/conf-available/fqdn.conf
  • 作ったファイルの中に、
ServerName exampleserver

と記載

  • 有効にする
$ sudo a2enconf fqdn
  • 再起動
$ sudo service apache2 restart
  • ブラウザで localhostと入力し、動作の確認をする
  • Ubuntu default pageと表示されればOK

PHP インストール

  • PHPをインストールする
$ sudo apt install php7.2 libapache2-mod-php7.2
  • ドキュメントルートにphpinfo()を記載し、ブラウザから動作確認をする
$ sudo nano /var/www/html/info.php
<?php
  phpinfo();
?>

参考記事

MacにVirtualBoxでUbuntuを立てる方法【画像での解説つき】
【Ubuntu 18.04 LTS Server】Apache2とPHP7.2を動かす

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[個人開発] 英語リーディングをサポートするツールを作った

AnnoReaderという英語学習者向けのサービスを作りました。

このサービスは、英文にさまざまな付加情報(アノテーションと呼びます)を追加し、複合語の範囲やフレーズの修飾関係などが可視化することで、、英文をより読みやすくします。主に英語の学習者が、日常的な英文リーディングの時に補助ツールとして使うことを想定しています。

test.png

この記事は、このサービスの開発のモチベーション実装など技術的な話を紹介します。同じように個人でサービス作りたいと思っている or 作ってる方の参考になればうれしいです。

使い方

annoreader.comにアクセスして、テキストエリアに英文を入力するだけです。試しに英語を勉強した人なら多くの人が知っている有名な以下のフレーズ

She sells seashells by the seashore. The shells she sells are surely seashells. So if she sells shells on the seashore, I’m sure she sells seashore shells.

これを使ってみると、このようになります

test2.png

また、Google ChromeMozilla Firefoxのブラウザ拡張もあります。日常的に使う場合はこちらの方が便利です。

モチベーションとゴール

エンジニアにとって、英文を読むことは大事なスキルの一つです。Webサイトであったり、論文であったり、README.mdであったり、色々なタイプの英語ドキュメントを読む必要があります。ただ、私も含め英語が苦手な人にとってなかなか大変な作業です。

その中で、私が一つ困ったのが「長文や複雑な文になると理解に時間がかかる or 内容を理解できない」ということです。特に文意を間違えて捉えてそれに気づかないまま読み進めてしまうと、読み終えたときに「あれ?この筆者は何を言っているんだろう?」と疑問だけが残ったりします。そうして翻訳ツールを使ってみると、自分が全く違った理解をしていたことに気づくのです。

このシチュエーションでは、単語を知っていることはもちろん、文の構造を正しく把握することも重要です。どれが主語で、どれが動詞で、どれが目的語なのか、そういった基本的なことはもちろん、節や句の修飾関係も、正しく見つけないといけません。

初見の英文に対してこれらを行うのは、学習者にとって非常に大変な作業です。経験が重要になるからです。AnnoReaderはこの「文の構造の把握」をシステムで解決したかったのです。

以前にMediumに詳細を書いたので、興味がありましたらそちらもご覧ください。

実装

実装面で個人的に面白かったポイントなどを紹介します。

システム概要

AnnoReaderのシステムは、Webサーバーがフロントにあり裏側にAPI的なサーバーを置くような、良くある一般的なWebアプリの構成です。

Untitled Diagram (1).png

当初AnnoReaderは、私が自分用に開発したツールでした。なのでリリース後も自宅サーバーの片隅で動かしていましたが、先月くらいからすべてのコンポーネントをコンテナにしてGKE上で稼働させるようになりました。ただ、NLPサーバーはメモリを数GB使うのでクラウド上で動かすと高コストです。なのでこれだけ自宅サーバー上に残しています。各サーバーの役割は後述します。

Webまわり

PHPフレームワークのLaravelを使っています。JSは特にフレームワークは使わずにゴリゴリ書いてます。

Localicationがあるので言語切り替えの実装が楽だったのと、フロントエンド系のツールが統合されているのが非常に便利でした(僕はあまりWebpackの設定とかが得意ではないので)。

また、AnnoReaderのアノテーション表示はすべてHTMLとCSSで実現しています。

cap0.png

たとえば上記の文でClauseとなる standing over there はこれで一つのSPANタグで、囲っている点線などはCSSの border: 1px dotted $color で表現されています。

cap1.png

ほかにも、"the man"はPhraseになり、これも一つのSPANタグです。CSSで青い下線を表示しています。

cap2.png

HTML+CSSで作ったのにはいくつか理由がありますが、一つ大きなメリットは他のブラウザ拡張がAnnoReader上のテキストにそのまま使えることです。典型的なのはWeblioのようなオンライン辞書です。

test3.png

オンライン辞書やGoogle翻訳は、英文を読む人の多くが使っているツールだと思います。AnnoReaderによってアノテーションが付与された英文においても同様のツールが使えるようにすることで、リーディングがより捗るようになります。

英文を構造化する

AnnoReaderは英文の構造を視覚化するわけですから、システム上も英文を単なる文字列では無く構造化して扱う必要があります。現状でAnnoReaderは英文を以下のパートに分解して扱っています。

  • Sentence(文) 先頭からピリオドまで
  • Clause(節) 複数の語の集まりでS+Vの構造を持つもの
  • Phrase(句) 複数の語の集まり
  • Word(語) スペースで区切られた一つの単語

そして、Sentenceを最上位、Wordを最下位のオブジェクトとした親子関係を作ります。例えば、

Go is an open source programming language that makes it easy to build simple, reliable, and efficient software. (golang.orgのトップページから引用)

この文章の場合、

  • Sencente: 先頭からピリオドまで
  • Clause : 「Go is an open source programming language」や「that makes it easy 〜 software」
  • Phrase : [an open source programming language」や「efficient software」
  • Word : Go, is, an, open, source など

このようになります。

ClauseやPhraseは他のClause, Phrase, Wordを修飾することがあります。例えば上記の文だと that makes it easy to build simple, reliable, and efficient software. というClauseが language というWordを修飾します。

このように構造化された英文情報をJSONで返すAPIを用意しています。エンドポイントは https://anooreader.com/api/annotateです。annoreader.comのWebフォームやブラウザ拡張は裏でこのAPIを利用しています。試しにcurlで直接叩くと、

curl 'https://annoreader.com/api/annotate' -H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' --data 'q=Go+is+an+open+source+programming+language+that+makes+it+easy+to+build+simple%2C+reliable%2C+and+efficient+software.'

レスポンスはこうなります

アノテーションの生成

英文の構造を決定してアノテーションを構築する処理で、AnnoReaderの重要な実装の一つです。先ほどの図で言うと、赤枠の部分です。

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34343135322f31643432356463642d633933332d353635662d353633622d6437343664326332653261332e706e672.png

ここではオープンソースのNLPライブラリであるspaCyを利用しています。NLPはNatulal Language Processingの略で自然言語処理と呼ばれています。たとえば単語の品詞(名詞、動詞など)を特定する処理や、単語間の修飾関係(Dependency Parsingといいます)を導いたりする処理があります。Qiitaにも専用タグがあって多くの記事がありますね。

spaCyを使うとどんなことができるのか、以下のサイトでオンライン上で試してみることもできます。実行するとわかりますが、英文に関する非常に多くの情報が取れます。

Demos · Explosion

実際の処理では、アノテーションサーバーがNLPサーバーにテキストを渡して、spaCyのレスポンスを受け取ります。そしてレスポンスを元に、情報の取捨選択、節や単語の修飾関係の整理、複数の単語の統合などを行い、最終的なアウトプットを作ります。

アノテーションサーバーはGoで実装されていて、HTTPサーバーの部分はGinを使っています。

ほかのNLPライブラリ

AnnoReaderは当初spaCyではなくStanford CoreNLPを使って開発していました。これはおそらく一番ポピュラーなNLPライブラリで、ドキュメントや利用事例も多く使いやすかったです。。Java実装ですが、言語バインディングも多くあるので使いやすく、この辺が人気だったりするんでしょうかね。私が書いたGoバインディングもあります。

go-corenlp

AnnoReaderの使い方であれば、精度もパフォーマンスもspaCyと並んで十分でした。ただ私がJavaを読めないので、最終的にはPython実装のspaCyを利用するようになりました。

GKE

現状は以下のような使い方をしています。個人サービスでGKEを使うのはtoo muchな印象ですし、実際VPSで十分なのですが、私のKubernetesの勉強を兼ねて使っています。(なのでNode数は1でケチってます...)

# kubectl get secret
NAME                  TYPE                                  DATA   AGE
annoreader-ssl        kubernetes.io/tls                     2      9d
default-token-tq44z   kubernetes.io/service-account-token   3      13d

# kubectl get configmap
NAME           DATA   AGE
nginx-config   3      13d

# kubectl get deployment
NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
annotator        1         1         1            1           9d
memcached-pods   1         1         1            1           13d
web              1         1         1            1           12d

# kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
annotator-bf75679f9-jrwqt         1/1     Running   0          31h
mackerel-agent-rcr4r              1/1     Running   0          37h
memcached-pods-76c6d6dbc8-2jh6b   1/1     Running   0          37h
web-86dc7df9c6-qnf7b              2/2     Running   0          29h

# kubectl get services
NAME                TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
annoreader-web      NodePort    10.0.15.53   <none>        80:32029/TCP   12d
annotator-service   ClusterIP   10.0.6.157   <none>        3939/TCP       9d
memcached-service   ClusterIP   10.0.1.92    <none>        11211/TCP      13d

# kubectl get ingress
NAME                 HOSTS            ADDRESS         PORTS     AGE
annoreader-ingress   annoreader.com   ***.***.***.***   80, 443   12d

おわりに

個人でのサービス開発は、すべて自分の責任で好きに作れるのでとても楽しいですね。

もしAnnoReaderが皆さんの学習の役に立ったら、開発者としてとてもうれしいです(ぜひフィードバックをください)。

おまけ

Twitterで教えてもらいましたが、こんな不思議な文があるそうです。文法的に正しいですが、これはAnnoReaderでは解析失敗します。面白いですね。

Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.

Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo - Wikipedia

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XAMPPを使ったローカル開発[バージョン : OS X XAMPP-VM(PHP 7.2.10)]

PHP開発の簡単な利用方法についての記述。

XAMPPのインストール方法については、Google検索すると簡単にまとめられている。

◯バージョンは、Mac OS X XAMPP-VM (PHP 7.2.10)です。
2018年10月現在の最新バージョンです。
スクリーンショット 2018-10-07 14.33.09.png

◯PHP記述に使うテキストエディターは、Sublime Textです。
【参考サイト:https://php-archive.net/php/php-texteditor-mac/

スクリーンショット 2018-10-08 11.01.39.png

◯利用方法

① General タブ
「 Start 」ボタンをクリック
(開発言語の細かい操作は、Servicesにて可能)
スクリーンショット 2018-10-08 11.11.31.png

「 Start 」ボタン押すとこんな感じ↓
スクリーンショット 2018-10-08 11.15.26.png

② Network タブ
使用ローカルホスト(localhost)を選択し、Enableをクリック
今回は、localhost:8080 -> 80(Over SSH)を使用
スクリーンショット 2018-10-08 11.18.50.png

実際やってみるとこんな感じ↓
スクリーンショット 2018-10-08 11.23.00.png

③ Volumes タブ
Mountをクリック
スクリーンショット 2018-10-08 11.27.24.png

デスクトップにXAMPPのディレクトリが表示される
スクリーンショット 2018-10-08 11.00.16.png

ディレクトリを開き、[htdocs]内にprojectフォルダを作成。

スクリーンショット 2018-10-08 11.51.22.png

④ PHPの記述
Sublime Textを開き、適当に記述する
スクリーンショット 2018-10-08 11.41.23.png

test.phpなど適当に名前をつけて、projectフォルダに保存。

④ WEBブラウザにて表示
Google Chromeにて
localhost:8080/project/test.php
上記URLをブラウザで検索。
スクリーンショット 2018-10-08 11.53.51.png

ブラウザでの表示完了!

バージョンアップがかなり多いので、初心者には苦戦する内容ですが、
チャレンジしてみると案外シンプルでした!

【参考にしたサイトURL : https://blanche-toile.com/web/mac-xampp

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

strptime は環境によって挙動が違う

(この記事は 地平線に行く とのマルチポストです)

PHP には、日付文字列をパースするための strptime という関数があります。
これを使って、Sun, 19 Apr 2015 11:43:30 GMT という文字列を %a, %d %b %Y %H:%M:%S %Z というフォーマットでパースした結果、以下の通り環境によって異なる結果になりました。
strptime は環境によって挙動が違うんですね。

OS tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Linux 115 3 19 11 43 30 "GMT"
Mac 115 3 19 20 43 30 ""

Linux の場合は GMT で返ってきましたが、Mac の場合は JST で返ってきました。
(どちらも、OS のタイムゾーン設定は JST です)

どうしてこのような違いが起きたのか、ドキュメントを確認してみました。

PHP のドキュメント

ちゃんとOSにより挙動が異なることが明記されています。

注意:
内部では、 この関数はシステムの C ライブラリ関数 strptime() をコールしています。 このライブラリ関数は、OS によって挙動が異なることがあります。
PHP: strptime - Manual

ただ、どのように挙動が異なるかは記載されていません。そのため、次に各OSのドキュメントを確認してみました。

Linux の場合

フォーマットの指定として %Z (タイムゾーン) を指定することはできるが、無視すると書かれています。

原文:
For reasons of symmetry, glibc tries to support for strptime() the same format characters as for strftime(3). (In most cases the corresponding fields are parsed, but no field in tm is changed.) This leads to
%Z The timezone name.

日本語:
対象性のために、glibc では strptime() に strftime(3) と同じフォーマット文字をサポートさせようとしている。多くの場合、対応するフィールドが解釈されるが、tm フィールドは変更されない。使用可能なフォーマット文字を以下に示す。
%Z タイムゾーン名

そのため、文字列中のタイムゾーンの指定が GMT だろうが JST だろうが、以下のように同じ結果が返ってきます。「11時って書いてあるんだから11時だろ」という解釈です。

入力 tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Sun, 19 Apr 2015 11:43:30 GMT 115 3 19 11 43 30 "GMT"
Sun, 19 Apr 2015 11:43:30 JST 115 3 19 11 43 30 "JST"

Mac (BSD系UNIX) の場合

現地のタイムゾーンに変換してくれるそうです。

原文:
The strptime() function parses the string in the buffer buf, according to the string pointed to by format, and fills in the elements of the struc-ture structure ture pointed to by tm. The resulting values will be relative to the local time zone.

日本語:
strptime() 関数は、formatが指す文字列に従ってバッファbuf内の文字列を解析し、timeptr が指す構造体の要素にあてはめます。 結果の値は現地のタイムゾーンを基準にしたものになります。

Mac OS X Manual Page For strptime(3)

ただし、文字列中のタイムゾーンとして指定できるのは GMT かローカルタイムゾーン (今回の場合は JST) のみだそうです。

原文:
The %Z format specifier only accepts time zone abbreviations of the local time zone, or the value "GMT". This limitation is because of ambiguity due to of the over loading of time zone abbreviations. One such example is EST which is both Eastern Standard Time and Eastern Australia Summer Time.

日本語:
%Z フォーマット指定子は、ローカルタイムゾーンのタイムゾーンの省略形、または値 "GMT"のみを受け入れます。 この制限は、タイムゾーンの略語のオーバーロードによるあいまいさのためです。 そのような例の1つは、東部標準時と東オーストラリアの夏時間の両方であるESTです。

やってみると、こうなりました。「GMT の11時って書いてあるんだから、現地時間の20時だろ」という解釈です。

入力 tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Sun, 19 Apr 2015 11:43:30 GMT 115 3 19 20 43 30 ""
Sun, 19 Apr 2015 11:43:30 JST 115 3 19 20 43 30 ""

OSによらず、同じ結果になるようにしたい場合は?

マニュアルに書いてありました。date_parse_from_format 関数を使いましょう。

date_parse_from_format() はこの問題の影響を受けないので、PHP 5.3.0 以降ではこちらの関数を使うことを推奨します。
PHP: strptime - Manual

まとめ

PHP は Write once, Run anywhere じゃなかった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSの参照渡し、値渡し。PHPの参照渡し、値渡し。

JSの配列やobjectは参照渡し。
stirngやintegerは値渡し。

PHPは通常、値渡し。
&をつけて&$hogeとすると参照渡し。

JSが参照渡しになることを知らなくて、なんでだー、Vueのせいかとかいろいろやった挙げ句、バグの原因がこれだったので、あーとなった。

参考
JavaScript の配列やオブジェクトは参照渡しになる…バグを生む落とし穴
http://neos21.hatenablog.com/entry/2018/05/20/080000

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSの参照渡しのような、値渡し。PHPの参照渡し、値渡し。

JSの配列やobjectは参照渡しのような値渡し。
stirngやintegerは値渡し。

@shiracamus さんにコメントで教えていただきました。

正確には、JSには値渡ししかなくて、参照渡しはできないです。オブジェクトを代入した変数にはオブジェクトの参照値が保持されていて、保持している値=オブジェクトの参照値 を渡すので、あくまで値渡しなのです。

PHPは通常、値渡し。
&をつけて&$hogeとすると参照渡し。

JSが参照渡しになることを知らなくて、なんでだー、Vueのせいかとかいろいろやった挙げ句、バグの原因がこれだったので、あーとなった。

参考
JavaScript の配列やオブジェクトは参照渡しになる…バグを生む落とし穴
http://neos21.hatenablog.com/entry/2018/05/20/080000

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サービスコンテナとは

サービスコンテナとは

・各クラスのインスタンスの生成方法を保持し,インスタンスの管理を行っている
・ビジネスロジックから要求されると、手順にしたがってインスタンスを生成し、返却する。(この返却することをresolveという)
・ビジネスロジックはサービスコンテナに対してインスタンスを要求するだけ
(newをしてインスタンスを生成する必要はない)

(通常は)

sample.php
  $app_user = new AppUser;
  $app_user->hoge()

のようにしなくてはいけないが、サービスコンテナを使うことで、

=>インスタンス管理を気にせずビジネスロジックだけに専念できる

流れとしては以下のようになる
1,サービスコンテナにbind
(=この名前で要求されたら、このクラスのインスタンスを返すという決まりを登録すること)
2,サービスコンテナに対して、要求
3,resolve
(要求した名前がサービスコンテナにbindされてれば、それを返却される)

bind(登録)方法

新たにprovider内にクラスを作成してもいいが、
すでに用意されているapp/Providers/AppServiceProvider.phpに書いていく.
ここには、boot()とregister()がある。

アプリ起動処理=>ビジネスロジック

という流れでアプリケーションは動くが、
このメソッドはアプリ起動処理時に呼ばれるため、基本的にこのboot()かregister()に書いていくことになるが、
バインド処理は基本的にregister()にかく。

app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        app()->bind(Hoge::class,function(){
           return new Hoge();
        })
    }
}

上記はHoge::classという名前で、Hogeクラスのインスタンスを取得する例である。
このようにbindすることで、laravelアプリのどこからでも、以下のようにして、インスタンスを取得できるようになる

resolve方法

bindされたclassをresolveするには、makeメソッドか、appヘルパを使用する

resolve.php
<?php
$hoge = app()->make(Hoge::class)
$hoge = app(Hoge::class)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開始タグの前に改行を入れるべからず

PHP で作られたバッチを実行すると、なぜか、最初に空白行が出力されました。

starttag.php
<?php
require_once 'starttag_called.php';
echo "abc\n";
starttag_called.php
<?php
echo "called\n";
          ← 空白行
called
abc

原因

starttag_called.php で、 <?php の前に空白行があることが原因です。

公式マニュアル にも書いてあるとおりです。

つまり、開始タグと終了タグで囲まれている 箇所以外のすべての部分は、PHP パーサに無視されます。

なので、 <?php の前に文字が書いてあっても、普通に出力されます。

starttag_called.php
xyz
<?php
echo "called\n";
xyz      ← 空白行
called
abc

感想

もともと HTML にコードを埋め込めることが PHP のウリだったのだから、考えてみれば当たり前の動きです。
ただ、「どこで空白行を出力しているんだ?」と、がんばって PHP のコードを読んでもわからず、デバッグしてようやく気づいたという・・・。 require_once で読み込んでいる先のコードが原因だったのも厄介でした。

ということで、本日の教訓。
開始タグの前に余計な改行を入れるな

「それがなんだってんだよ。とんでもないクソ記事だな!」
「ネタがなさすぎて、この程度の記事しか書けなかったのだ。自分的にはがんばったつもりだが、これが今の実力というものなのだろう」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Laravel] Font Awesomeがクロスドメイン制約で読み込まれない時の対処法

初めてLaravelをデプロイした時Font Awesomeで少し詰まったのでメモ。

エラー内容

以下の記事を参考にLaravel Mixを利用してFont Awesomeを導入したけど、Herokuにデプロイしてみると本番環境のみFont Awesomeが読み込まれないエラーに遭遇。
https://readouble.com/laravel/5.8/ja/mix.html
https://qiita.com/algi_nao/items/5d2befae8f367050ae7f

エラー文には以下のような表示がされている。
スクリーンショット 2019-07-28 8.31.49.png

どうやらブラウザ(http://〜)から自サーバーに置いているフォントファイル(https://〜)を読み込む時にhttpとhttpsの違いによりクロスドメインになってしまっている。

なんでや(・_・?)

解決方法1

フォントファイルへアクセスするときはクロスドメイン制約を解除する方法。

Laravel Mixを使用した場合、以下のディレクトリにフォントファイルが格納されている。なので、このフォントファイルへのアクセスを許可してやればいい。
スクリーンショット 2019-07-28 9.09.19.png

.htaccessファイルによってこのフォントファイル群へのクロスドメイン制約を解除します。
上記のfontsディレクトリ直下に.htaccessファイルを作成し、以下のように記述。

public/fonts/.htaccess
<FilesMatch "\.(ttf|svg|eot|woff|woff2)$">
  <IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin "*"
  </IfModule>
</FilesMatch>

拡張子を指定してフォントファイルへの制約を解除しています。
これを再デプロイすることでフォントファイルが読み込まれるはずです。

それでも表示されない場合は、ブラウザのキャッシュが残っていると表示されない場合があるので以下の記事を参考にキャッシュをクリア。
https://tekito-style.me/columns/laravel-css-changes#3_Laravel

解決方法2

httpsに統一してクロスドメインにならないようにする方法。こちらの方が根本的な解決方法になる。

herokuは基本的にhttpsが有効になっているはずなのにアプリ内に生成されているリンクを見るとhttpになっている。
下のリンクではパスの生成にroute()を使っているが、生成されたパスがhttpになってしまっている。
そのため、リンクから遷移するとブラウザの表示がhttpになってしまい、クロスドメインになってしまう。

スクリーンショット 2019-07-28 18.54.21.png

参考記事によれば、以下のようにAppServiceProvider.phpに追記することで本番環境のみhttpsを強制することができる。

app/Providers/AppServiceProvider.php
    public function boot()
    {
       // 本番環境(Heroku)でhttpsを強制する
       if (\App::environment('production')) {
           \URL::forceScheme('https');
    }

これにより、生成されるリンクがhttpsになるためクロスドメインではなくなりフォントファイルが読み込まれるはずです。

参考

ファイルを別ドメインから読み込ませようとしたら怒られた
XHR2でサブドメインのワイルドカードOriginに対してCORSを許可する設定、他。
Laravel5.7: Herokuにデプロイする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPの学習をしながらツイッターの様な物を作ってみた

はじめに

1か月程HTML.CSS.JSを勉強した後PHPの知識ほぼ0の状態からツイッターのようなものを作ってみました
制作にかかった期間は2ヵ月(約200時間程)

github
https://github.com/nyann123/snspoi

制作した物
https://snspoi.herokuapp.com

下記を入力していただければログインできます

email: test@test
pass: test12

目的

  • PHPの学習
  • webサービスで使われる技術を触ってみる(使ってみる)
  • 就職活動に向けての制作物作り

スペック

■ 言語
HTML、CSS(scss)、JavaScript(jQuery)、PHP
■ DB
MySQL
■ サーバー
heroku

各種機能

ユーザー関連

  • ユーザー登録
  • ログイン・ログアウト
  • 退会
  • プロフィール編集
  • ユーザーフォロー機能

投稿関連

  • 投稿機能
  • 投稿削除
  • 投稿のお気に入り登録

機能解説っぽいの

関連部分のソースを切り取ってます

  • ユーザー登録

ユニークidを生成してそれをurlにくっつけてメールで送信&DBに登録
urlを開いたらDB照合して問題なければ残りの情報を入力して登録完了
メール送信はsendgridを使って実装しました

ソース
  1. 最初のメールアドレス入力画面
signup_first.php
<?php
require_once("config.php");
require_once('vendor/autoload.php');


//ログイン中はアクセスできないように
check_logged_in();

if(isset($_SESSION['send_to'])){
  $send_to = $_SESSION['send_to'];
  unset($_SESSION['send_to']);
}

//送信されていれば新規登録処理
if(!empty($_POST)){
  $email = $_POST['email'];

  valid_email($email);
  set_flash('error',$error_messages);

  if(empty($error_messages)){

  // ユニークid生成
  $unique_id = uniqid(rand());
  //有効期限(時)
  $limit_time = 2;

  $date = new DateTime();
  $date->setTimeZone(new DateTimeZone('Asia/Tokyo'));
  $date->modify('+'.$limit_time.'hours');

    try {
      $dbh = dbConnect();
      //emailが存在していればINSERT、なければUPDATE
      $sql = 'INSERT INTO provisional_users(email,unique_id,limit_time)
              VALUES(:email,:unique_id,:limit_time)
              ON DUPLICATE KEY UPDATE
              email = :email, unique_id = :unique_id, limit_time = :limit_time';
      $stmt = $dbh->prepare($sql);
      $stmt->execute(array(':email' => $email,
                           ':unique_id' => $unique_id,
                           ':limit_time' => $date->format('Y-m-d H:i:s')));

      // メール送信処理
      $from = new SendGrid\Email(null, "snspoi@example.com");
      $subject = "登録案内メール";
      $to = new SendGrid\Email(null, $email);
      $content = new SendGrid\Content("text/plain",
       "下記のURLにアクセスして、登録を完了させてください\n
       https://snspoi.herokuapp.com/signup_second.php?u_id=${unique_id} ");
      $mail = new SendGrid\Mail($from, $subject, $to, $content);

      $apiKey = getenv('SENDGRID_API_KEY');
      $sg = new \SendGrid($apiKey);

      $response = $sg->client->mail()->send()->post($mail);
      $_SESSION['send_to'] = $email;
    } catch (\Exception $e) {
      error_log('エラー発生:' . $e->getMessage());
      set_flash('error',ERR_MSG1);
    }
  }
  reload();
}

$site_title = '新規登録';
$js_file = 'signup_first';
require_once('head.php');
 ?>

<body>
  <?php require_once('header.php') ?>
  <div class="form_container border_white">
    <h2 class="page_title">新規登録</h2>

    <?php if (isset($flash_messages)): ?>
      <?php foreach ((array)$flash_messages as $message): ?>
        <p class ="flash_message <?= $flash_type ?>"><?= $message?></p>
      <?php endforeach ?>
    <?php endif ?>

    <!-- メール送信前に表示 -->
    <?php if (empty($send_to)): ?>
      <div class="form_inner">
        <form action="#" method="post">
          <span class="flash_cursor"></span>

          <label for="email">メールアドレス</label><br>
          <input id="email" type="text" name="email">
          <span class="js_error_message"></span><br>

          <button id="js_btn" class="btn blue send_btn" type="submit" disabled>メールを送信する</button>
          <a href="login_form.php" class="login link">>>ログインページへ</a>

        </form>
      </div>
    <!-- メール送信後に表示 -->
    <?php else: ?>
      <div class="send_to">
        <p><?= $send_to ?></p>
        <p>にメールを送信しました。<br>メールを確認して登録を完了してください。</p>
      </div>
      <a href="login_form.php" class="link">>>ログインページへ</a>
    <?php endif; ?>
  </div>
<?php require_once('footer.php'); ?>

  
2. 届いたメールのリンククリック後

signup_second.php
<?php
require_once("config.php");

//ログイン中はアクセスできないように
check_logged_in();

$now = new DateTime();
$now->setTimeZone(new DateTimeZone('Asia/Tokyo'));
$now->format('Y-m-d H:i:s');



$u_id = $_GET['u_id'];

$dbh = dbConnect();
$sql = 'SELECT *
        FROM provisional_users
        WHERE unique_id = :u_id';
$stmt = $dbh->prepare($sql);
$stmt->execute(array(':u_id' => $u_id));
$provisional_user = $stmt->fetch();

// 仮登録確認
if ($provisional_user){

  // 登録期限確認
  if($provisional_user['limit_time'] > $now->format('Y-m-d H:i:s')){
    $authentication = true;
  }else{
    set_flash("error",'有効期限切れです。最初からやり直してください。');
    header('Location:login_form.php');
    exit();
  }

}else{
  set_flash("error",'不正なアクセスです');
  header('Location:login_form.php');
  exit();
}

//エラー発生時の入力保持
set_old_form_data('name');

if(!empty($_POST)){
  require_once("signup_process.php");
}

$site_title = '新規登録';
$js_file = 'signup_second';
require_once('head.php');
 ?>

<body>
  <?php require_once('header.php') ?>
  <div class="form_container border_white">
    <h2 class="page_title">新規登録</h2>

    <?php if (isset($flash_messages)): ?>
      <?php foreach ((array)$flash_messages as $message): ?>
        <p class ="flash_message <?= $flash_type ?>"><?= $message?></p>
      <?php endforeach ?>
    <?php endif ?>


    <?php if (isset($authentication)): ?>
      <div class="form_inner">
        <form action="#" method="post">
          <span class="flash_cursor"></span>

          <label for="name">ユーザー名 <span>※最大8文字</span></label><br>
          <input id="name" type="text" name="name" value="<?php if (isset($oldname)) echo h($oldname); ?>">
          <span class="js_error_message"></span><br>

          <label for="password">パスワード <span>※半角英数6文字以上</span> </label><br>
          <input id="password" type="password" name="pass">
          <span class="js_error_message"></span><br>

          <button id="js_btn" class="btn blue" type="submit" disabled>登録</button>
        </form>
      </div>
    <?php endif; ?>

  </div>
<?php require_once('footer.php'); ?>

  
3. 2で送信した後の処理

signup_process.php
<?php
$name = $_SESSION['name'] = $_POST['name'];
$pass = $_SESSION['pass'] = $_POST['pass'];
$email = $provisional_user['email'];

//入力のバリデーション
valid_name($name);
valid_password($pass);

set_flash('error',$error_messages);

//エラーがなければ次の処理に進む
if(empty($error_messages)){

  $date = new DateTime();
  $date->setTimeZone(new DateTimeZone('Asia/Tokyo'));

  try {
    // 新規登録
    $dbh = dbConnect();
    $sql = 'INSERT INTO users(name,email,password,created_at)
            VALUES(:name,:email,:password,:created_at)';
    $stmt = $dbh->prepare($sql);
    $stmt->execute(array(':name' => $name ,
                         ':email' => $email ,
                         ':password' => password_hash($pass,PASSWORD_DEFAULT),
                         ':created_at' => $date->format('Y-m-d H:i:s')));
      //フォーム入力保持用のsession破棄
      unset($_SESSION['name']);
      unset($_SESSION['pass']);

      //登録したユーザーをログインさせる
      $sesLimit = 60*60;
      $_SESSION['login_date'] = time();
      $_SESSION['login_limit'] = $sesLimit;
      $_SESSION['user_id'] = $new_user_id = $dbh->lastInsertId();

      //仮登録テーブルから削除
      $sql = 'DELETE
              FROM provisional_users
              WHERE email = :email';
      $stmt = $dbh->prepare($sql);
      $stmt->execute(array(':email' => $email));

      set_flash('sucsess','登録が完了しました');

      header("Location:user_page.php?page_id=${new_user_id}&type=main");
      exit();
  } catch (\Exception $e) {
    error_log('エラー発生:' . $e->getMessage());
    set_flash('error',ERR_MSG1);
  }
}

reload();


  

  • ログイン・ログアウト

入力された情報をDB照合、セッションを使ってログイン
この時に退会済み(delete_flg=1)であれば復元させる
(ツイッターを参考にユーザー復元機能をつけてみたがアカウントを完全に消すことができなくなってしまった)

ソース
  • ログイン画面
login_form.php
<?php
require_once('config.php');

//ログイン中はアクセスできないように
check_logged_in();

// 送信されていればログイン処理
if(!empty($_POST)){
  require_once('login_process.php');
}

$site_title = 'ログイン';
$js_file = 'login';
require_once('head.php');
?>

<body>
<?php require_once('header.php'); ?>
  <div class="form_container border_white">
    <h2 class="page_title">ログイン</h2>

    <?php if (isset($flash_messages)): ?>
      <?php foreach ((array)$flash_messages as $message): ?>
        <p class ="flash_message <?= $flash_type ?>"><?= $message?></p>
      <?php endforeach ?>
    <?php endif ?>

    <div class="form_inner">
      <span class="flash_cursor"></span>
      <form action="#" method="post">
        <input id="email" type="email" name="email" placeholder="メールアドレス">
        <input id="password" type="password" name="password" placeholder="パスワード"><br>
        <label id="pass_save" for="checkbox">
        <input id="checkbox" type="checkbox" name="pass_save">ログインを維持する
        </label><br>
        <button id="login_btn" class="btn" type="submit" value="ログイン">ログイン</button>
        <a href="signup_first.php" class="signup link">>>新規登録はこちら</a>
      </form>
    </div>
  </div>
<?php require_once('footer.php'); ?>
  • ログイン処理
login_process.php
<?php
$email = $_POST['email'];
$password = $_POST['password'];
$pass_save = (isset($_POST['pass_save'])) ? true : false;

// メールのバリデーション
if( empty($email) ){
  $error_messages['email'] = "メールアドレスを入力してください";
}
// パスワードのバリデーション
if ( empty($password) ) {
  $error_messages['pass'] = "パスワードを入力してください";
}
//バリデーションエラーがなければ処理を続ける
if(empty($error_messages)){

  //emailでユーザー情報を取得
  try {
    $dbh = dbConnect();
    $sql = "SELECT password,id,delete_flg
            FROM users
            WHERE email = :email";
    $stmt = $dbh->prepare($sql);
    $stmt->execute(array(':email' => $email));
    $user = $stmt->fetch(PDO::FETCH_ASSOC);


    //パスワードでユーザー認証
    if (isset($user) && password_verify($password, $user['password'])) {

      //delete_flgが1ならユーザー復元処理
      if($user['delete_flg']){
                try {
          if(query_result(change_delete_flg($user,0)));

          // ログインさせる
          login($user['id'],$pass_save);
          set_flash('sucsess','登録されていたユーザーを復元しました');


          header("Location:user_page.php?page_id=${user['id']}&type=main");
          exit();
        } catch (\Exception $e) {
          error_log('エラー発生:' . $e->getMessage());
          $error_messages = ERR_MSG1;
        }
      }else{
        // ログインさせる
        login($user['id'],$pass_save);
        set_flash('sucsess','ログインしました');


        header("Location:user_page.php?page_id=${user['id']}&type=main");
        exit();
      }
    }else{
      $error_messages[] = "メールアドレス又はパスワードが間違っています。";
    }
  } catch (\Exception $e) {
    error_log('エラー発生:' . $e->getMessage());
    $error_messages = ERR_MSG1;
  }
}
set_flash('error',$error_messages);


header('Location:login_form.php');
  • ログアウト処理
logout_process.php
<?php
require_once("config.php");


require_once('auth.php');

//ログイン中ならログアウトさせる
if (isset($_SESSION['user_id'])) {
  session_destroy();
  $_SESSION = array();

  header('Location:login_form.php');
}


  

  • 退会機能

説明とかは省略してワンボタン即退会
復元できるように論理削除で実装してます

ソース
withdraw.php
<?php
require('config.php');


require('auth.php');

$current_user = get_user($_SESSION['user_id']);

// post送信されていた場合
if(!empty($_POST['withdraw'])){
  change_delete_flg($current_user,1);

 //セッション削除
  session_destroy();
  $_SESSION = array();

  header("Location:login_form.php");
  exit();
}

$site_title = '退会';
require('head.php');
?>

<body>
  <?php require('header.php'); ?>
  <div class="container flex">
    <!-- メニュー -->
  <?php require_once('setting_menu.php'); ?>

    <div class="setting_container border_white">
      <h2 class="page_title withdraw">退会</h2>
      <form action="" method="post">
        <button class="btn red" name="withdraw" value="withdraw" type="submit">退会する</button>
      </form>
    </div>

  </div>
  <?php require('footer.php'); ?>


  

  • プロフィール編集

名前、アイコン画像、150字以内のコメントが設定できます
ajaxを使用して入力情報を送信、DB登録
PHP側でバリデーションを通して結果を返して画面に表示するようにしてます

ソース
user_page.js
  //プロフィール編集
  $('.profile_save').on('click',function(e){
    e.stopPropagation();
    var name_data = $('.profile .edit_name').val() || $('.slide_prof .edit_name').val() || '',
        comment_data = $('.profile .edit_comment').val() || $('.slide_prof .edit_comment').val() || '',
        icon_data = $('.profile_icon > img').attr('src'),
        user_id = $(this).data('user_id');

    $.ajax({
      type: 'POST',
      url: 'ajax_edit_profile.php',
      dataType: 'json',
      data: {name_data: name_data,
             comment_data: comment_data,
             icon_data: icon_data,
             user_id: user_id}
    })
    .done(function(data){
      // エラーメッセージがあれば表示
      if(data['flash_message']){
        show_slide_message(data['flash_type'],data['flash_message']);
      }else{
        location.reload();
      }
    }).fail(function(){
      location.reload();
    });
  });
ajax_edit_profile.php
<?php
require_once('config.php');
require_once('auth.php');

if(isset($_POST)){

  $user_id = $_POST['user_id'];
  $name_data = $_POST['name_data'];
  $comment_data = $_POST['comment_data'];
  $icon_data = $_POST['icon_data'];

  // バリデーション
  valid_name($name_data);
  if(isset($error_messages['name'])){
    $return = array('flash_type' => 'flash_error',
                    'flash_message' => $error_messages['name']);
    echo json_encode($return);
   exit();
  }
  // バリデーション
  if(valid_length($comment_data,100)){
    $return = array('flash_type' => 'flash_error',
                    'flash_message' => 'コメントは100文字以内で入力してください');
    echo json_encode($return);
    exit();
  }

  // バリデーションOKならDB更新処理
  try {
    $dbh = dbConnect();
    $sql = "UPDATE users
            SET name = :name_data,
                profile_comment = :comment_data,
                user_icon = :icon_data
            WHERE id = :user_id";
    $stmt = $dbh->prepare($sql);
    $stmt->execute(array(':name_data' => $name_data,
                         ':comment_data' => $comment_data,
                         ':icon_data' => $icon_data,
                         ':user_id' => $user_id));

    set_flash('sucsess','プロフィールを更新しました');
    echo json_encode('sucsess');
  } catch (\Exception $e) {
    set_flash('error',ERR_MSG1);
  }
}


  

  • プロフィール画像アップロード

画像加工部分はほぼコピペ
ajaxで送信した画像をアイコンサイズにトリミング、縮小して(画像の中央固定)
加工した画像をAWSのS3にアップロードして保存&画面に表示します

tes.gif

ソース
user_page.js
   $('.icon_upload').on('change',function(e){
   e.stopPropagation();
   var max_file_size = 10485760;

   // ファイルサイズ制限
   if (max_file_size < this.files[0].size){
     show_slide_message('flash_error','ファイルサイズは10M以下にしてください');
     $(this).val('');
     // ファイルタイプ制限
   }else if(!this.files[0].name.match(/.(jpg|jpeg|png)$/i)){
     show_slide_message('flash_error','対応していない拡張子です');
   }else{
     //読み込み中のgif表示
     $('.profile_icon > img').attr('src','img/loading.gif');
     //読み込み中はボタンを押せないように
     $('.profile_save').prop("disabled", true);
     // フォームデータを取得
     var formdata = new FormData($('#icon_form').get(0));
     $.ajax({
       type: 'POST',
       url: 'ajax_icon_create.php',
       dataType: 'json',
       data: formdata,
       cache       : false,
       contentType : false,
       processData : false
     }).done(function(data){
       // アイコンを返ってきた加工済みアイコンと入れ替える
       $('.profile_icon > img').attr('src',data);
       $('.edit_icon').css('display','none');
       $('.profile_save').prop('disabled', false);
     }).fail(function(){
      location.reload();
     });
   }
  });
ajax_icon_create.php
<?php
require_once('vendor/autoload.php');
require_once('config.php');
require_once('auth.php');


$max_file_size = 10485760;
$permit_filetype = array('jpg','jpeg','png',);

$file =$_FILES['icon']['name'];

if($max_file_size < $_SERVER["CONTENT_LENGTH"]){
  set_flash('error','ファイルサイズは10M以下にしてください');
  exit();
//拡張子確認
}else if(!in_array(pathinfo($file, PATHINFO_EXTENSION),$permit_filetype)){
  set_flash('error','対応していない拡張子です');
  exit();
}

//元の画像を取得する
$file = $_FILES["icon"]["tmp_name"];

 // 画像タイプ判定用
$image_type = exif_imagetype($file);

//元の画像を読み込む
if ($image_type === IMAGETYPE_JPEG){
  $baseImage = ImageCreateFromJPEG($file);
}else if($image_type === IMAGETYPE_PNG){
  $baseImage = ImageCreateFromPNG($file);
}

// 画像一時保存先のパス
$save_path = sha1_file($file).image_type_to_extension($image_type);

//元画像の縦横の大きさを比べてどちらかにあわせる
// なおかつ縦横の差をコピー開始位置として使えるようセット
list($w, $h) = getimagesize($file);

if($w > $h){
    $diff  = ($w - $h) * 0.5;
    $diffW = $h;
    $diffH = $h;
    $diffY = 0;
    $diffX = $diff;
}elseif($w < $h){
    $diff  = ($h - $w) * 0.5;
    $diffW = $w;
    $diffH = $w;
    $diffY = $diff;
    $diffX = 0;
}elseif($w === $h){
    $diffW = $w;
    $diffH = $h;
    $diffY = 0;
    $diffX = 0;
}
//アイコンのサイズ
$iconW = 64;
$iconH = 64;

//アイコンになる土台の画像を作る
$new_icon = imagecreatetruecolor($iconW, $iconH);

//アイコンになる土台の画像に合わせて元の画像を縮小しコピーペーストする
imagecopyresampled($new_icon, $baseImage, 0, 0, $diffX, $diffY, $iconW, $iconH, $diffW, $diffH);

imagejpeg($new_icon, $save_path);

//================================
//  S3アップロード
//================================

$bucket_version = 'latest';
$bucket_region = 'ap-northeast-1';
$bucket_name = getenv('S3_BUCKET_NAME');

$credentials = [
  'key' => getenv('AWS_ACCESS_KEY_ID'),
  'secret' => getenv('AWS_SECRET_ACCESS_KEY'),
];

$s3 = new Aws\S3\S3Client([
    'credentials' => $credentials,
    'region'  => $bucket_region,
    'version' => $bucket_version,
]);

$params = [
  'ACL' => 'public-read',
  'Bucket' => $bucket_name,
  'Key' => 's3/'.$save_path,
  'SourceFile'   => $save_path,
  'ContentType' => mime_content_type($save_path)
];

 $result = $s3 -> putObject($params);

  //読み取り用のパスを返す
 $path = $result['ObjectURL'];


echo json_encode($path);


  

  • ユーザーフォロー機能

ajaxを使って実装
同じボタンでフォローと解除を両方できるようにしています
フォローするとフォローしたユーザーの投稿がタイムラインに表示されるように

follow.gif

ソース
user_page.js
  // フォロー登録、解除処理
  $(document).on('click','.follow_btn',function(e){
    e.stopPropagation();
    var $this = $(this),
        $follow_count = $('.profile_count + .follow > a > .count_num'),
        $follower_count = $('.profile_count + .follower > a > .count_num'),
        profile_user_id = $('.profile_user_id').val();
        user_id = $this.prev().val();

    $.ajax({
        type: 'POST',
        url: 'ajax_follow_process.php',
        dataType: 'json',
        data: { profile_user_id: profile_user_id,
                user_id: user_id}
    }).done(function(data){
      // php側の処理に合わせてボタンを更新する
      // php側でエラーが発生したらリロードしてエラーメッセージを表示させる
      if(data === "error"){
        location.reload();
      }else if(data['action'] ==="登録"){
        $this.toggleClass('following')
        $this.text('フォロー中');
      }else if(data['action'] ==="解除"){
        $this.removeClass('following');
        $this.removeClass('unfollow')
        $this.text('フォロー');
      }
      // プロフィール内のカウントを更新する
      $follow_count.text(data['follow_count']);
      $follower_count.text(data['follower_count']);
    }).fail(function() {
      location.reload();
    });
  });
ajax_follow_process.php
<?php
require_once('config.php');
require_once('auth.php');

if(isset($_POST)){

  $current_user = get_user($_SESSION['user_id']);

  $user_id = $_POST['user_id'];
  $profile_user_id = $_POST['profile_user_id'] ?? $user_id;

  // 自分をフォローできないように
  if ( !is_myself($user_id)){
    // すでに登録されているか確認して登録、削除のSQL切り替え
    if(check_follow($current_user['id'],$user_id)){
      $action = '解除';
      $flash_type = 'error';
      $sql ="DELETE
              FROM relation
              WHERE :follow_id = follow_id AND :follower_id = follower_id";
    }else{
      $action = '登録';
      $flash_type = 'sucsess';
      $sql ="INSERT INTO relation(follow_id,follower_id)
              VALUES(:follow_id,:follower_id)";
    }
    try {
      $dbh = dbConnect();
      $stmt = $dbh->prepare($sql);
      $stmt->execute(array(':follow_id' => $current_user['id'] , ':follower_id' => $user_id));

      $return = array('action' => $action,
                      'follow_count' => current(get_user_count('follow',$profile_user_id)),
                      'follower_count' => current(get_user_count('follower',$profile_user_id)));
      echo json_encode($return);

    } catch (\Exception $e) {
      error_log('エラー発生:' . $e->getMessage());
      set_flash('error',ERR_MSG1);
      echo json_encode("error");
    }
  }
}


  

  • 投稿機能

300字以内で好きな内容を投稿できます
10行以上の内容は省略して全文表示ボタンを出すようにしてます

post.gif

ソース
  • 投稿処理部分
post_process.php
<?php
$post_content = $_POST['content'];

//投稿の長さチェック
valid_post($post_content);

set_flash('error',$error_messages);

if (empty($error_messages)){

  $date = new DateTime();
  $date->setTimeZone(new DateTimeZone('Asia/Tokyo'));

  try {
    $dbh = dbConnect();
    $sql = "INSERT INTO posts(user_id,post_content,created_at)
            VALUES(:user_id,:post_content,:created_at)";
    $stmt = $dbh->prepare($sql);
    $stmt->execute(array(':user_id' => $current_user['id'] ,
                         ':post_content' => $post_content,
                         ':created_at' => $date->format('Y-m-d H:i:s')));

    set_flash('sucsess','投稿しました');
  } catch (Exception $e) {
    error_log('エラー発生:' . $e->getMessage());
    set_flash('error',ERR_MSG1);
  }
}

reload();
  • 投稿省略
post_list.php
<?php if (substr_count($post['post_content'],"\n") +1 > 10): ?>
  <button class="show_all">続きを表示する</button>
<?php endif ?>
user_page.js
  // 投稿全文表示
  $(document).on('click','.show_all',function(){
    // 省略されている投稿の高さを取得
    var omit_height = $(this).parent().height();
    //投稿の省略を解除
    $(this).prev().removeClass('ellipsis');
    // 全文表示された投稿の高さを取得
    var all_height = $(this).parent().height();
    //一度高さを戻して
    $(this).parent().height(omit_height);
    //スライドで全文表示させる
    $(this).parent().animate({
      height: all_height
    },"slow","swing");

    //ボタンを消す
    $(this).remove()
  });
base_style.scss
.ellipsis{
  overflow: hidden;
  padding: 0;
  display: -webkit-box;
   -webkit-box-orient: vertical;
   -webkit-line-clamp: 10;
}


  

  • 投稿削除

削除前に一度モーダルを開いて確認を挟んでから削除するようにしています

delete.gif

ソース
post_delete_process.php
<?php
$post_id = $_POST['post_id'];
$user_id = $_POST['user_id'];

//ログイン中のユーザーの投稿であれば削除処理
if (is_myself($user_id)) {
  try {
    $dbh = dbConnect();
    $sql = "DELETE
            FROM posts
            WHERE id = :id";
    $stmt = $dbh->prepare($sql);
    $stmt->execute(array(':id' => $post_id));

    set_flash('error','削除しました');
  } catch (\Exception $e) {
    error_log('エラー発生:' . $e->getMessage());
    set_flash('error',ERR_MSG1);
  }
}else{
  set_flash('error','他人の投稿は削除できません');
}

reload();


  

  • 投稿のお気に入り登録

ajaxを使って実装
お気に入り登録したものは一覧からいつでも確認できます
また各投稿にはお気に入り登録されているカウントが表示されます

favorite.gif

ソース
user_page.js
  //お気に入り登録処理
  $(document).on('click','.favorite_btn',function(e){
    e.stopPropagation();
    var $this = $(this),
        $profile_count = $('.profile_count + .favorite > a > .count_num'),
        page_id = get_param('page_id'),
        post_id = $this.prev().val();

    $.ajax({
        type: 'POST',
        url: 'ajax_post_favorite_process.php',
        dataType: 'json',
        data: { page_id: page_id,
                post_id: post_id}
    }).done(function(data){
      // php側でエラーが発生したらリロードしてエラーメッセージを表示させる
      if(data ==="error"){
        location.reload();
      }else{
        // プロフィール内のカウントを更新する
        $profile_count.text(data['profile_count']);
        // 投稿内のカウントを更新する
        $this.next('.post_count').text(data['post_count']);
        // アイコンを切り替える
        $this.children('i').toggleClass('fas');
        $this.children('i').toggleClass('far');
      }
    }).fail(function() {
      location.reload();
    });
  });
ajax_post_favorite_process.php
<?php
require_once('config.php');
require_once('auth.php');

if(isset($_POST)){

  $user_id = $_POST['user_id'];
  $name_data = $_POST['name_data'];
  $comment_data = $_POST['comment_data'];
  $icon_data = $_POST['icon_data'];

  // バリデーション
  valid_name($name_data);
  if(isset($error_messages['name'])){
    $return = array('flash_type' => 'flash_error',
                    'flash_message' => $error_messages['name']);
    unset($error_messages);
    echo json_encode($return);
   exit();
  }
  // バリデーション
  if(valid_length($comment_data,100)){
    $return = array('flash_type' => 'flash_error',
                    'flash_message' => 'コメントは100文字以内で入力してください');
    echo json_encode($return);
    exit();
  }

  // バリデーションOKならDB更新処理
  try {
    $dbh = dbConnect();
    $sql = "UPDATE users
            SET name = :name_data,
                profile_comment = :comment_data,
                user_icon = :icon_data
            WHERE id = :user_id";
    $stmt = $dbh->prepare($sql);
    $stmt->execute(array(':name_data' => $name_data,
                         ':comment_data' => $comment_data,
                         ':icon_data' => $icon_data,
                         ':user_id' => $user_id));

    set_flash('sucsess','プロフィールを更新しました');
    echo json_encode('sucsess');
  } catch (\Exception $e) {
    set_flash('error',ERR_MSG1);
  }
}


  

  • 投稿の読み込み

ajaxを使用して画面下部までスクロールしたときに追加で読み込むようにしています

ソース
user_page.js
//最後までスクロールしたら投稿を取得する
  var offset= 10;
  var more_post_flg = 0 ;
  $(window).on('scroll', function () {
  var doch = $(document).innerHeight(), //ページ全体の高さ
      winh = $(window).innerHeight(), //ウィンドウの高さ
      bottom = doch - winh, //ページ全体の高さ - ウィンドウの高さ = ページの最下部位置
      query = get_param('page_id') || get_param('query') || "";
      page_type = get_param('type'),
      end_post_flg = 0;

    if (bottom * 0.9 <= $(window).scrollTop() && flg === 0) {
      more_post_flg = 1;

      $.ajax({
        type: 'POST',
        url: 'ajax_more_data.php',
        dataType: 'json',
        data: { offset: offset,
                query: query,
                page_type: page_type}
      }).done(function(data){
        offset += data['data_count'];
        //投稿が返されていれば追加する
        if (data[('data_html')]) {
          $('.main_items').append(data['data_html']);
        }else{
          end_post_flg = 1
        }
        // 投稿が全て読み込み終わったらトップへ戻るボタンを追加する
        if(end_post_flg === 1 && !$('.item_container:last').next().hasClass('gotop')){
          $('.main_items').append("<button type='button' class='gotop'>トップへ戻る <i class='fas fa-caret-up'></i></button>");
        }
        more_post_flg = 0
      }).fail(function(){

      })
    }
  });
ajax_more_data.php
<?php
require_once('config.php');

if(isset($_POST)){

  if(isset($_SESSION['user_id'])){
    $current_user = get_user($_SESSION['user_id']);
  }
  $query = $_POST['query'];
  $page_type = $_POST['page_type'];
  $offset_count = $_POST['offset'];

  // ページに合わせた投稿、ユーザーを取得
  switch ($page_type) {
    case 'main':
    $posts =  get_posts($query,'my_post',$offset_count);
    $type = 'post';
      break;

    case 'favorites':
    $posts = get_posts($query,'favorite',$offset_count);
    $type = 'post';
      break;

    case 'timeline':
    $posts = get_posts($current_user['id'],'timeline',$offset_count);
    $type = 'post';
      break;

    case 'follows':
    $id_type ='follower';
    ${$id_type."user"} = get_users($query,'follows',$offset_count*2);
    $type ='user';
      break;

    case 'followers':
    $id_type = 'follow';
    ${$id_type."user"} = get_users($query,'followers',$offset_count*2);
    $type ='user';
      break;

    case 'search':
    $id_type = '';
    ${$id_type."user"} =  get_users($query,'search',$offset_count*2);
    $type ='user';
    break;

  }

  //取得した投稿もしくはユーザーを変数にいれる
  $data = $posts ?? ${$id_type."user"};

  // 取得した数
  $data_count = count($data);
  //取得したデータをHTMLに加工して返す
    ob_start();
    require($type.'_list.php');
    $data_html = ob_get_contents();
    ob_end_clean();

  echo json_encode(array('data_html' => $data_html, 'data_count' => $data_count));
}

データベース

image.png

WWW SQL Designerを使用して図にしてます

使用したツール等

  • git

記録用&バックアップ用
commitしてpush

  • gulp

Sassのコンパイル用に導入
結構面白かったのでその後もいろいろ導入もしてみました

  • composer

heroku使用時に要求されたので導入
要求された物を入れただけでほぼノータッチ

さいごに

約2ヵ月間の勉強の成果になります
各機能を個別に作って組み合わせたりしてるうちに掲示板を作ろう、ツイッターに近づけてみようという目標が決まり最終的にこうなりました

いろいろ粗いところも多いと思いますがある程度の機能は実装できたと思いますしエラーへの対処なども身に着けられたかなと思います
今後の予定としてフレームワーク(laravel予定)を使用して自作アプリの制作を考えてます

ここまで読んでくださりありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mac、Nginx、php-fpm7.2 の環境で Laravel 5.8を動かす

経緯

上記環境でLaravelアプリケーションが動かなかった
(トップページは表示されたものの、ログインページが 404 Not Found となった)
ので、設定ファイルを変更したときのメモ。

Mac、Nginx、php-fpm7.2構築方法はこちら
macにbrewでnginxとPHP7.2をインストールした
https://qiita.com/uneri/items/46bfeeabf77d76abb3ba

概要

Nginxのデフォルトの設定ファイルをコピーして、以下2箇所を書き換える。

詳細

デフォルトの設定ファイルをコピー

cd /usr/local/etc/nginx/
cp nginx.conf.default nginx.conf
vim nginx.conf

1箇所目

変更前

nginx.conf
      location / {
            root   html;
            index  index.html index.htm;
        }

変更後(xxxxはアプリケーションのパス)

nginx.conf
      location / {
            root /usr/local/var/www/xxxx/public;
            index index.php index.html
            try_files $uri $uri/ /index.php;
        }

2箇所目

変更前

nginx.conf
       #location ~ \.php$ {
       #    root           html;
       #    fastcgi_pass   127.0.0.1:9000;
       #    fastcgi_index  index.php;;
       #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
       #    include        fastcgi_params;
       #}

変更後(xxxxはアプリケーションのパス)

nginx.conf
        location ~ \.php$ {
            root /usr/local/var/www/xxxx/public;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include        fastcgi_params;
        }

Nginxを再起動する

brew services restart nginx

ブラウザでアプリケーションにアクセスする

http://localhost:8080/

参考にした情報

All Laravel routes "not found" on nginx
https://www.digitalocean.com/community/questions/all-laravel-routes-not-found-on-nginx

MacでNingx、PHP7.1、php-fpmで環境構築(メモ)
https://qiita.com/nogizaka46/items/42a6ea1c634985f98564

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む