- 投稿日:2019-07-28T22:14:32+09:00
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
というメソッドを呼び出しているだけです。まとめ
->ってなんやねんと思いましたが、要は左の中にある右のものを取り出しているだけなんですね。私含め、この記事を読んでいる方が少しでも理解していただければ幸いです。
- 投稿日:2019-07-28T21:59:28+09:00
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の危険性あり
- 投稿日:2019-07-28T21:08:27+09:00
php-master-changes 2019-07-27
今日は BOM 付 UTF-8 から BOM を削除する修正があった!
2019-07-27
zebastian: file encoding cleanup: remove bom in win32 files
- https://github.com/php/php-src/commit/a2b2aaa67a834d014d2f800cc65c86cb557cac61
- [7.4~]
- BOM 付 UTF-8 という不条理なファイルが 2 つあったので BOM を削除
- ソースコード全体に dos2unix して出たのがこの 2 ファイルだったらしい
- 投稿日:2019-07-28T21:04:59+09:00
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 rootApache インストール
- 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
と表示されればOKPHP インストール
- 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を動かす
- 投稿日:2019-07-28T18:20:09+09:00
[個人開発] 英語リーディングをサポートするツールを作った
AnnoReaderという英語学習者向けのサービスを作りました。
このサービスは、英文にさまざまな付加情報(アノテーションと呼びます)を追加し、複合語の範囲やフレーズの修飾関係などが可視化することで、、英文をより読みやすくします。主に英語の学習者が、日常的な英文リーディングの時に補助ツールとして使うことを想定しています。
この記事は、このサービスの開発のモチベーション実装など技術的な話を紹介します。同じように個人でサービス作りたいと思っている 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.
これを使ってみると、このようになります
また、Google Chrome と Mozilla Firefoxのブラウザ拡張もあります。日常的に使う場合はこちらの方が便利です。
モチベーションとゴール
エンジニアにとって、英文を読むことは大事なスキルの一つです。Webサイトであったり、論文であったり、README.mdであったり、色々なタイプの英語ドキュメントを読む必要があります。ただ、私も含め英語が苦手な人にとってなかなか大変な作業です。
その中で、私が一つ困ったのが「長文や複雑な文になると理解に時間がかかる or 内容を理解できない」ということです。特に文意を間違えて捉えてそれに気づかないまま読み進めてしまうと、読み終えたときに「あれ?この筆者は何を言っているんだろう?」と疑問だけが残ったりします。そうして翻訳ツールを使ってみると、自分が全く違った理解をしていたことに気づくのです。
このシチュエーションでは、単語を知っていることはもちろん、文の構造を正しく把握することも重要です。どれが主語で、どれが動詞で、どれが目的語なのか、そういった基本的なことはもちろん、節や句の修飾関係も、正しく見つけないといけません。
初見の英文に対してこれらを行うのは、学習者にとって非常に大変な作業です。経験が重要になるからです。AnnoReaderはこの「文の構造の把握」をシステムで解決したかったのです。
以前にMediumに詳細を書いたので、興味がありましたらそちらもご覧ください。
実装
実装面で個人的に面白かったポイントなどを紹介します。
システム概要
AnnoReaderのシステムは、Webサーバーがフロントにあり裏側にAPI的なサーバーを置くような、良くある一般的なWebアプリの構成です。
当初AnnoReaderは、私が自分用に開発したツールでした。なのでリリース後も自宅サーバーの片隅で動かしていましたが、先月くらいからすべてのコンポーネントをコンテナにしてGKE上で稼働させるようになりました。ただ、NLPサーバーはメモリを数GB使うのでクラウド上で動かすと高コストです。なのでこれだけ自宅サーバー上に残しています。各サーバーの役割は後述します。
Webまわり
PHPフレームワークのLaravelを使っています。JSは特にフレームワークは使わずにゴリゴリ書いてます。
Localicationがあるので言語切り替えの実装が楽だったのと、フロントエンド系のツールが統合されているのが非常に便利でした(僕はあまりWebpackの設定とかが得意ではないので)。
また、AnnoReaderのアノテーション表示はすべてHTMLとCSSで実現しています。
たとえば上記の文でClauseとなる
standing over there
はこれで一つのSPANタグで、囲っている点線などはCSSのborder: 1px dotted $color
で表現されています。ほかにも、"the man"はPhraseになり、これも一つのSPANタグです。CSSで青い下線を表示しています。
HTML+CSSで作ったのにはいくつか理由がありますが、一つ大きなメリットは他のブラウザ拡張がAnnoReader上のテキストにそのまま使えることです。典型的なのはWeblioのようなオンライン辞書です。
オンライン辞書や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の重要な実装の一つです。先ほどの図で言うと、赤枠の部分です。
ここではオープンソースのNLPライブラリであるspaCyを利用しています。NLPはNatulal Language Processingの略で自然言語処理と呼ばれています。たとえば単語の品詞(名詞、動詞など)を特定する処理や、単語間の修飾関係(Dependency Parsingといいます)を導いたりする処理があります。Qiitaにも専用タグがあって多くの記事がありますね。
spaCyを使うとどんなことができるのか、以下のサイトでオンライン上で試してみることもできます。実行するとわかりますが、英文に関する非常に多くの情報が取れます。
実際の処理では、アノテーションサーバーがNLPサーバーにテキストを渡して、spaCyのレスポンスを受け取ります。そしてレスポンスを元に、情報の取捨選択、節や単語の修飾関係の整理、複数の単語の統合などを行い、最終的なアウトプットを作ります。
アノテーションサーバーはGoで実装されていて、HTTPサーバーの部分はGinを使っています。
ほかのNLPライブラリ
AnnoReaderは当初spaCyではなくStanford CoreNLPを使って開発していました。これはおそらく一番ポピュラーなNLPライブラリで、ドキュメントや利用事例も多く使いやすかったです。。Java実装ですが、言語バインディングも多くあるので使いやすく、この辺が人気だったりするんでしょうかね。私が書いたGoバインディングもあります。
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
さすがにこれは無理でした。ゴメンなさい。
— やへがき やくも〔主〕「カシアス内調」 (@YAHEGAKI_Yakumo) July 7, 2019
Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.
バファロー(地名)のバファロー(野牛。一頭め)がバファローする(脅す)バファローのバファロー(二頭め)はバファローのバファロー(三頭め)を脅す。https://t.co/bhziVnNuDo.
- 投稿日:2019-07-28T18:02:59+09:00
XAMPPを使ったローカル開発[バージョン : OS X XAMPP-VM(PHP 7.2.10)]
PHP開発の簡単な利用方法についての記述。
XAMPPのインストール方法については、Google検索すると簡単にまとめられている。
◯バージョンは、Mac OS X XAMPP-VM (PHP 7.2.10)です。
2018年10月現在の最新バージョンです。
◯PHP記述に使うテキストエディターは、Sublime Textです。
【参考サイト:https://php-archive.net/php/php-texteditor-mac/ 】◯利用方法
① General タブ
「 Start 」ボタンをクリック
(開発言語の細かい操作は、Servicesにて可能)
② Network タブ
使用ローカルホスト(localhost)を選択し、Enableをクリック
今回は、localhost:8080 -> 80(Over SSH)を使用
ディレクトリを開き、[htdocs]内にprojectフォルダを作成。
④ PHPの記述
Sublime Textを開き、適当に記述する
test.phpなど適当に名前をつけて、projectフォルダに保存。
④ WEBブラウザにて表示
Google Chromeにて
localhost:8080/project/test.php
上記URLをブラウザで検索。
ブラウザでの表示完了!
バージョンアップがかなり多いので、初心者には苦戦する内容ですが、
チャレンジしてみると案外シンプルでした!【参考にしたサイトURL : https://blanche-toile.com/web/mac-xampp 】
- 投稿日:2019-07-28T17:57:41+09:00
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 が指す構造体の要素にあてはめます。 結果の値は現地のタイムゾーンを基準にしたものになります。ただし、文字列中のタイムゾーンとして指定できるのは
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 じゃなかった。
- 投稿日:2019-07-28T11:53:36+09:00
JSの参照渡し、値渡し。PHPの参照渡し、値渡し。
JSの配列やobjectは参照渡し。
stirngやintegerは値渡し。PHPは通常、値渡し。
&をつけて&$hogeとすると参照渡し。JSが参照渡しになることを知らなくて、なんでだー、Vueのせいかとかいろいろやった挙げ句、バグの原因がこれだったので、あーとなった。
参考
JavaScript の配列やオブジェクトは参照渡しになる…バグを生む落とし穴
http://neos21.hatenablog.com/entry/2018/05/20/080000
- 投稿日:2019-07-28T11:53:36+09:00
JSの参照渡しのような、値渡し。PHPの参照渡し、値渡し。
JSの配列やobjectは参照渡しのような値渡し。
stirngやintegerは値渡し。@shiracamus さんにコメントで教えていただきました。
正確には、JSには値渡ししかなくて、参照渡しはできないです。オブジェクトを代入した変数にはオブジェクトの参照値が保持されていて、保持している値=オブジェクトの参照値 を渡すので、あくまで値渡しなのです。
PHPは通常、値渡し。
&をつけて&$hogeとすると参照渡し。JSが参照渡しになることを知らなくて、なんでだー、Vueのせいかとかいろいろやった挙げ句、バグの原因がこれだったので、あーとなった。
参考
JavaScript の配列やオブジェクトは参照渡しになる…バグを生む落とし穴
http://neos21.hatenablog.com/entry/2018/05/20/080000
- 投稿日:2019-07-28T11:42:07+09:00
サービスコンテナとは
サービスコンテナとは
・各クラスのインスタンスの生成方法を保持し,インスタンスの管理を行っている
・ビジネスロジックから要求されると、手順にしたがってインスタンスを生成し、返却する。(この返却することを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)
- 投稿日:2019-07-28T10:25:27+09:00
開始タグの前に改行を入れるべからず
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.phpxyz <?php echo "called\n";xyz ← 空白行 called abc感想
もともと HTML にコードを埋め込めることが PHP のウリだったのだから、考えてみれば当たり前の動きです。
ただ、「どこで空白行を出力しているんだ?」と、がんばって PHP のコードを読んでもわからず、デバッグしてようやく気づいたという・・・。require_once
で読み込んでいる先のコードが原因だったのも厄介でした。ということで、本日の教訓。
開始タグの前に余計な改行を入れるな「それがなんだってんだよ。とんでもないクソ記事だな!」
「ネタがなさすぎて、この程度の記事しか書けなかったのだ。自分的にはがんばったつもりだが、これが今の実力というものなのだろう」
- 投稿日:2019-07-28T09:24:45+09:00
[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どうやらブラウザ(http://〜)から自サーバーに置いているフォントファイル(https://〜)を読み込む時にhttpとhttpsの違いによりクロスドメインになってしまっている。
なんでや(・_・?)
解決方法1
フォントファイルへアクセスするときはクロスドメイン制約を解除する方法。
Laravel Mixを使用した場合、以下のディレクトリにフォントファイルが格納されている。なので、このフォントファイルへのアクセスを許可してやればいい。
.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になってしまい、クロスドメインになってしまう。参考記事によれば、以下のようにAppServiceProvider.phpに追記することで本番環境のみhttpsを強制することができる。
app/Providers/AppServiceProvider.phppublic function boot() { // 本番環境(Heroku)でhttpsを強制する if (\App::environment('production')) { \URL::forceScheme('https'); }これにより、生成されるリンクがhttpsになるためクロスドメインではなくなりフォントファイルが読み込まれるはずです。
参考
ファイルを別ドメインから読み込ませようとしたら怒られた
XHR2でサブドメインのワイルドカードOriginに対してCORSを許可する設定、他。
Laravel5.7: Herokuにデプロイする
- 投稿日:2019-07-28T09:16:55+09:00
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を使って実装しました
ソース
- 最初のメールアドレス入力画面
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にアップロードして保存&画面に表示します
ソース
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を使って実装
同じボタンでフォローと解除を両方できるようにしています
フォローするとフォローしたユーザーの投稿がタイムラインに表示されるように
ソース
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_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; }
- 投稿削除
削除前に一度モーダルを開いて確認を挟んでから削除するようにしています
ソース
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を使って実装
お気に入り登録したものは一覧からいつでも確認できます
また各投稿にはお気に入り登録されているカウントが表示されます
ソース
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)); }データベース
WWW SQL Designerを使用して図にしてます
使用したツール等
- git
記録用&バックアップ用
commitしてpush
- gulp
Sassのコンパイル用に導入
結構面白かったのでその後もいろいろ導入もしてみました
- composer
heroku使用時に要求されたので導入
要求された物を入れただけでほぼノータッチさいごに
約2ヵ月間の勉強の成果になります
各機能を個別に作って組み合わせたりしてるうちに掲示板を作ろう、ツイッターに近づけてみようという目標が決まり最終的にこうなりましたいろいろ粗いところも多いと思いますがある程度の機能は実装できたと思いますしエラーへの対処なども身に着けられたかなと思います
今後の予定としてフレームワーク(laravel予定)を使用して自作アプリの制作を考えてますここまで読んでくださりありがとうございました!
- 投稿日:2019-07-28T05:58:42+09:00
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.conf1箇所目
変更前
nginx.conflocation / { root html; index index.html index.htm; }変更後(xxxxはアプリケーションのパス)
nginx.conflocation / { 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.conflocation ~ \.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ブラウザでアプリケーションにアクセスする
参考にした情報
All Laravel routes "not found" on nginx
https://www.digitalocean.com/community/questions/all-laravel-routes-not-found-on-nginxMacでNingx、PHP7.1、php-fpmで環境構築(メモ)
https://qiita.com/nogizaka46/items/42a6ea1c634985f98564