バックエンドとフロントエンドを行き来するWEBプログラマ―のメモ帳

WEBプログラマ―。バックエンドはPHP, MySQL, CentOS系, フロントエンドはJavaScript, jQuery, HTML, CSSで仕事してます。

Laravelの勉強でSNS「ねこぴく」というサイトを作ってみました

ブランクを克服するために、最近トレンドなPHPフレームワークのLaravelを使い、SNS「ねこぴく」というサイトを作ってみました

サイト

f:id:mashiro_ruka:20190704153317p:plain
ねこぴく

neko-pic.com

概要

猫の写真を他の人と共有するWEBサイトです。
機能は以下になります。
・お気に入り機能(いいね機能)
・マイページでアップロードした写真を管理(編集、削除)
・写真にコメントできる機能
・タグ検索機能(複数のタグを組み合わせることができるor検索)
・ログイン、ログアウト、登録
スマートフォン対応(Androidで確認)


スクリーンショット(PC版)

トップ画面

f:id:mashiro_ruka:20190704152726p:plain

ログイン画面

f:id:mashiro_ruka:20190704152748p:plain

登録画面

f:id:mashiro_ruka:20190704152804p:plain

検索画面

f:id:mashiro_ruka:20190704152837p:plain

アップロード画面

f:id:mashiro_ruka:20190704152857p:plain

マイページ画面

f:id:mashiro_ruka:20190704152915p:plain

お気に入り写真画面

f:id:mashiro_ruka:20190704152947p:plain

写真詳細画面

f:id:mashiro_ruka:20190704153015p:plain

写真情報編集画面

f:id:mashiro_ruka:20190704153035p:plain

プロフィール編集画面

f:id:mashiro_ruka:20190704153055p:plain

スクリーンショットスマホ版)

トップ画面

f:id:mashiro_ruka:20190705141006j:plain

ログイン前 メニュー

f:id:mashiro_ruka:20190705134559p:plain

ログイン中 メニュー

f:id:mashiro_ruka:20190705134617p:plain

検索画面

f:id:mashiro_ruka:20190705134649j:plain

アップロード画面

f:id:mashiro_ruka:20190705134726p:plain

写真詳細画面

f:id:mashiro_ruka:20190705142031j:plain

写真情報編集画面

f:id:mashiro_ruka:20190705142113j:plain

マイページ画面

f:id:mashiro_ruka:20190705141133j:plain

写真長押しで、いいね数とコメント - マイページ

f:id:mashiro_ruka:20190705134905p:plain

プロフィール編集画面

f:id:mashiro_ruka:20190705141219j:plain

お気に入り写真画面

f:id:mashiro_ruka:20190705141203j:plain

他のユーザーページ

f:id:mashiro_ruka:20190705141148j:plain

使用したツール・環境・技術

PHP 7.2
MySQL 8.0
・HTML
CSS
Javascript
Atom Editor
Photoshop CC
PowerShell
CentOS 7.6
jQuery 3.4.1
Putty
vagrant
・Virtual Box
Cyberduck
・ConoHa VPS
・git 2.19.2
github

Laravelで、テキスト内のhttpリンクの変換方法

Laravelにて、プロフィールなどの説明に、httpのリンクを途中で入れてあった場合、ちゃんとリンクされるようにしたい

こんな感じにしたい場合

理想はこの状態です。ちょっとハマってしまったため、メモします。
f:id:mashiro_ruka:20190704140707p:plain
使用アイコン:なゆみ様

このようなプロフィールになっているとします。
f:id:mashiro_ruka:20190704135901p:plain

間違った方法

Controllerで

<?php
~略~
'introduction' => $user->introduction

blade.php

<p>
 {{ $introduction }}
</p>

これだと、このようになってしまいます。
f:id:mashiro_ruka:20190704135911p:plain

解決方法

まずは、httpにaタグをつけます。
app/Libsフォルダに、PlanetextToUrl.phpを追加して以下の内容にします。

<?php

namespace App\Libs;

class PlanetextToUrl
{
    public static function convertLink($plane_text)
    {
        //URL抽出の正規表現
        $pattern = '/https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:@&=+$,%#]+/';

        //該当する文字列に処理
        $convert_text = preg_replace_callback($pattern,function ($matches) {

            return '<a href="'.$matches[0].'">'.$matches[0].'</a>';
        },htmlspecialchars($plane_text));

        return $convert_text;
    }
}

Controller内

<?php
//プロフィール紹介を文字列の中にurlがあったらコンバートするようにします。
$introduction = PlanetextToUrl::convertLink($user->introduction);

//出力します。 
'introduction' => $introduction

blade.php内では、以下のように改行させつつ、エスケープさせないように{!! $変数 !!}と記述します。
※nl2brで囲って、元のテキストと同じように、改行させます。

<p>
{!! nl2br($introduction) !!}
</p>

f:id:mashiro_ruka:20190704140707p:plain
これで、うまくいきました!

参考サイト

befool.co.jp

leben.mobi

お借りした素材サイト

www.ac-illust.com

LaravelのEloquentで集計関数Count()などを使うとき

LaravelのEloquentで集計関数を使いたい

データの準備

例えば、Tagsテーブルと、タグと写真を紐づける中間テーブルのTagmapsテーブルがあるとします。

・Tags
f:id:mashiro_ruka:20190702210612p:plain

・Tagmaps
f:id:mashiro_ruka:20190702210557p:plain

これを使って、タグ数表示を作りたいとします。

f:id:mashiro_ruka:20190703162306p:plain

その場合は、タグの数を集計する必要があります。
f:id:mashiro_ruka:20190702211752p:plain
そこでLaravelのEloquentで集計しようとしましたが、

<?php
//~略~
$tag_count_list = \DB::table('tagmaps')
            ->join('tags', 'tagmaps.tag_id', '=', 'tags.id')
            ->select('count(*), tags.name')
            ->groupBy('tag_id')
            ->orderBy('count(*)', 'desc')
            ->get();

select('count(*), ...')でエラー

ここで、上のように、Laravelで、集計関数をそのままSelectに放り込んでしまうとエラーが出ます。

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'count(*), tags.name' in 'field list' (SQL: select `count(*), tag ~略~

解決策 DB::raw('count(*) as tag_count, ~')

これを解決するには、

<?php
//~略~
->select('count(*), tags.name')

を下のように変えます。

<?php
//~略~
->select(DB::raw('count(*) as tag_count, tags.name'))
<?php
//~略~
$tag_count_list = DB::table('tagmaps')
->join('tags', 'tagmaps.tag_id', '=', 'tags.id')
->select(DB::raw('count(*) as tag_count, tags.name'))
->groupBy('tagmaps.tag_id')
->orderBy('tag_count', 'desc')
->get();

bladeファイルでは以下のように{{ $tag_count->tag_count }}で数が取得できるようになりました。

@foreach($tag_count_list as $tag_count)
  <a href="javascript:void(0)" onclick="this.parentNode.submit()" class="btn-tag m-1">
    {{ $tag_count->name }} ({{ $tag_count->tag_count }})
  </a>
@endforeach

参考サイト

yoshinorin.net

Laravelでミドルウェアでモバイル判定処理をして、全てのViewでisMobile変数を呼べるようにする

Laravelでミドルウェアでモバイル判定処理をして、全てのViewでisMobile変数を呼べるようにする

モバイル判定方法1.ヤバイパターン

まずは、ヤバイパターンとして、
モバイル判定処理を1つ1つコントローラに書いて、ビューに反映する場合
以下をすべてのコントローラーに書くのはなかなかヤバイですね…

<?php
//~略~
        $agent = app('agent');
        return view('index', [
            'isMobile' => $agent->isMobile(),
        ]);

モバイル判定方法2.楽なパターン

そこで、View::share で全てのビューで使うグローバル変数を定義して、
さらに、その処理をBeforeミドルウェアを使って、リクエストがきたら行う共通処理にしておきます。


その前に、composerからモバイルのエージェントの判定できるライブラリ
github.com

をインストールしておきます。

エージェント判定ライブラリをインストール
composer require jenssegers/agent

config/app.php

<?php
    'providers' => [
        Jenssegers\Agent\AgentServiceProvider::class, //追加
    ],
    'aliases' => [
        'Agent' => Jenssegers\Agent\Facades\Agent::class,  //追加
    ],

次にミドルウェアを作っていきます。

Middleware の作成
php artisan make:Middleware GetIsMobile
Kernel.phpにMiddleware を追加

全てのリクエストで処理をするため、$middlewareプロパティに追加します。

App\Http\Kernel.php

<?php
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
~略~
        \App\Http\Middleware\GetIsMobile::class, //<-追加
    ];

全てのHTTPリクエスト時にモバイル判定処理を走らせる準備ができました。

MiddlewareにView::shareでグローバル変数を定義していきます。

View::shareでグローバル変数

App/Http/Middleware/GetIsMobile.php

<?php

namespace App\Http\Middleware;

use Closure;
use Agent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View; //View::share用

class GetIsMobile
{
    public function handle($request, Closure $next)
    {
        $agent = app('agent');
        View::share(['isMobile' => $agent->isMobile()]);

        return $next($request);
    }
}

これで、どのページを開いてもblade内で
{{ $isMobile }}変数が使えるようになりました。

オシャレなブランドロゴを作れるLOGASTERを使ってみた!

www.777logos.com


Laravelの勉強で作ったサイト用に、ロゴを作ってみました。

こんな感じのロゴが作れますよ!
所要時間は5分かからなかったです。すごいサービスです。

f:id:mashiro_ruka:20190702153618p:plain

f:id:mashiro_ruka:20190702153630p:plain

ダウンロードして、Photoshopで色調を変えてみました。
なかなかいい感じに仕上がりました!
素晴らしいです!
f:id:mashiro_ruka:20190702153919p:plain

LaravelでGROUP BY clause and contains nonaggregated columnと出たとき

問題のSQL

<?php
$photos = Photo::
join ~省略~
->select('photos.*')
->groupBy('photos.id')

と、した時に
GROUP BY clause and contains nonaggregated column
は出現しました。

環境

Laravel 5.8
MySQL 8.0
PHP 7.2

原因

これはMySQL5.7からONLY_FULL_GROUP_BYがデフォルトで有効になっているためです。
qiita.com
元記事より引用

GROUP BYを使っていると、非集約カラムをSELECTで参照できなくなります。
つまりGROUP BY使っている場合はSELECTするものを全部GROUP BYに書け、とのことです。

そこで、/etc/my.cnfを編集していきたいところですが、
エラーが消えないなどハマってしまったためメモします。

解決

[mysqld]直下にsql_modeを書く!(重要)

手順

my.cnfの場所を探します

sudo find / -type f -ls  | fgrep my.cnf

4 -rw-r--r--   1 root     root         1352 Jun 26 20:18 /etc/my.cnf

と出たので、/etc/my.cnfにありますね!

sudo vi /etc/my.cnf
[mysqld]
sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
#
# Remove leading # and set to the amount of RAM for the most important data

my.cnfの一番下に書いてもなぜか反映されなかった。

保存して、mysqlサーバーを再起動する。

sudo systemctl restart mysqld

mysqlコンソールにて、ONLY_FULL_GROUP_BYが消えていることを確認します。

mysql> SELECT @@global.sql_mode;
+----------------------------------------------------------------------------------------------------+
| @@global.sql_mode                                                                                  |
+----------------------------------------------------------------------------------------------------+
| STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION |
+----------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

ONLY_FULL_GROUP_BYが消えていますので、WEBサイトにアクセスします。
すると、エラーGROUP BY clause and contains nonaggregated columnが消えました。

Laravelプロジェクトを本番サーバーで公開する時、Permission deniedでハマったら

The stream or file "/var/www/laravel/nekopic_website/storage/logs/laravel-2019-06-30.log" could not be opened: failed to open stream: Permission denied

f:id:mashiro_ruka:20190630110419p:plain

結論

権限の設定が必要

WEBサーバーを実行するapacheユーザーの権限が
logにないよ、ということでした。
さらに、Laravelのサイトにもある通り、
今後のためにstorageディレクトリと、bootstrap/cacheディレクトリにも権限を与えておく必要があります。

権限の設定法

こちらを参考に自分用にアレンジしました。

参考サイト

qiita.com

手順

まずはユーザーについてですが、以下のユーザーがいます
・hogeuser(puttyなどでログインしているユーザー)
sftpなどでファイル操作ができる
apache
webサーバーの実行ユーザー


Laravel用のユーザーグループを作成

sudo groupadd laravel

今作ったlaravelグループに、hogeuserとapacheを追加

sudo gpasswd -a hogeuser laravel
sudo gpasswd -a apache laravel

laravelプロジェクトに移動

cd /var/www/laravel/nekopic_website

パーミッションを、ディレクトリ:755、ファイル:644にする

sudo find ./ -type d -exec chmod 755 {} \;
sudo find ./ -type f -exec chmod 644 {} \;

所有グループを変更(エラーが修正される)

sudo chown -R :laravel ./storage
sudo chown -R :laravel ./bootstrap/cache

もう一度、storageとbootstrap/cache内のパーミッション
ディレクトリ:755、ファイル:644にする

sudo find ./storage -type d -exec chmod 775 {} \;
sudo find ./storage -type f -exec chmod 664 {} \;

sudo find ./bootstrap/cache -type d -exec chmod 775 {} \;
sudo find ./bootstrap/cache -type f -exec chmod 664 {} \;

SGIDを設定することにより、今後、storage内と、bootstrap/cache内で作成されたファイルやディレクトリの
所有グループはlaravelになるようにする。(logsのファイルは日ごとに作成されるため)

sudo find ./storage -type d -exec chmod g+s {} \;
sudo find ./bootstrap/cache -type d -exec chmod g+s {} \;

ACLのデフォルト設定で、
今後、storageとbootstrap/cache下にファイルやディレクトリが作られたら、
ディレクトリ:775、ファイル:664になる

sudo setfacl -R -d -m g::rwx ./storage
sudo setfacl -R -d -m g::rwx ./bootstrap/cache

再起動する

sudo shutdown -r now
重要!

サーバーを再起動したあと、忘れずにMySQLを再起動する。

sudo systemctl restart mysqld

そうしないと、ブラウザでアクセスした際に、
Connection refused (SQL: select count(*) as aggregate from
というエラーが出ます。

MySQL再起動は必ず行ってください。