20191002のPHPに関する記事は20件です。

【全6回】Laravel5.8でTwitterっぽいSNSツールを作る(第6回コメントといいね機能)

Laravelで始めるTwitter風(Twitterクローン)のSNSツール開発チュートリアル

概要

スクールとかの課題だったりLaravelを初めてみたいけど何を作ろうって迷ってる人向けによくあるTwitter風のWEBサイトを作ってみます。

前回

いよいよ最後となった第6回はコメントといいね機能を実装していきます?‍?

前提

  • PHPをある程度理解している
  • Homesteadをインストールしている
  • MVC構造をある程度理解している

環境

  • Mac
  • Homestead
  • Laravel 5.8

コメント機能

それではツイートに対してコメントを付けれる機能を実装していきます。

ルーティング

コメント機能を実装していくにあたりルーティングの定義をします。
コメント機能では保存のみの実装になるのでstoreのみ指定します。

routes/web.php
// ログイン状態
Route::group(['middleware' => 'auth'], function() {

    // 省略

    // コメント関連
    Route::resource('comments', 'CommentsController', ['only' => ['store']]);
});

Model

Controllerからバリデーションが通ってきたという前提で、データを保存していきます。
引数にはコメントしたユーザのユーザIDとなる$user_idとコメントのデータ$dataを設定します。

app/Models/Comment.php
    public function commentStore(Int $user_id, Array $data)
    {
        $this->user_id = $user_id;
        $this->tweet_id = $data['tweet_id'];
        $this->text = $data['text'];
        $this->save();

        return;
    }

Controller

では例によってCommentsController.phpファイルを--resourceで作成しましょう。

php artisan make:controller CommentsController --resource

ファイルを生成したら以下の内容で書いていきます。

基本的に【全6回】Laravel5.8でTwitterっぽいSNSツールを作る(第4回ツイートのCRUD機能)でやったツイートの保存と同じ流れなので説明は省きます

app/Http/Controllers/CommentsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Models\Comment;

class CommentsController extends Controller
{
    public function store(Request $request, Comment $comment)
    {
        $user = auth()->user();
        $data = $request->all();
        $validator = Validator::make($data, [
            'tweet_id' =>['required', 'integer'],
            'text'     => ['required', 'string', 'max:140']
        ]);

        $validator->validate();
        $comment->commentStore($user->id, $data);

        return back();
    }
}

View

早速Viewを作っていきたいのですが、コメントはツイートのViewを使用します。
ツイートの詳細画面からしか投稿できないという仕様なのでtweets/show.blade.phpに組み込んでいきます。

<!-- ここから下を変更してください -->の部分を上書きしてください。

resources/views/tweets/show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center mb-5">
        <div class="col-md-8 mb-3">
            <div class="card">
                <div class="card-haeder p-3 w-100 d-flex">
                    <img src="{{ asset('storage/profile_image/' .$tweet->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                    <div class="ml-2 d-flex flex-column">
                        <p class="mb-0">{{ $tweet->user->name }}</p>
                        <a href="{{ url('users/' .$tweet->user->id) }}" class="text-secondary">{{ $tweet->user->screen_name }}</a>
                    </div>
                    <div class="d-flex justify-content-end flex-grow-1">
                        <p class="mb-0 text-secondary">{{ $tweet->created_at->format('Y-m-d H:i') }}</p>
                    </div>
                </div>
                <div class="card-body">
                    {!! nl2br(e($tweet->text)) !!}
                </div>
                <div class="card-footer py-1 d-flex justify-content-end bg-white">
                    @if ($tweet->user->id === Auth::user()->id)
                        <div class="dropdown mr-3 d-flex align-items-center">
                            <a href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                <i class="fas fa-ellipsis-v fa-fw"></i>
                            </a>
                            <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                                <form method="POST" action="{{ url('tweets/' .$tweet->id) }}" class="mb-0">
                                    @csrf
                                    @method('DELETE')

                                    <a href="{{ url('tweets/' .$tweet->id .'/edit') }}" class="dropdown-item">編集</a>
                                    <button type="submit" class="dropdown-item del-btn">削除</button>
                                </form>
                            </div>
                        </div>
                    @endif
                    <div class="mr-3 d-flex align-items-center">
                        <a href="{{ url('tweets/' .$tweet->id) }}"><i class="far fa-comment fa-fw"></i></a>
                        <p class="mb-0 text-secondary">{{ count($tweet->comments) }}</p>
                    </div>
                    <div class="d-flex align-items-center">
                        <button type="" class="btn p-0 border-0 text-primary"><i class="far fa-heart fa-fw"></i></button>
                        <p class="mb-0 text-secondary">{{ count($tweet->favorites) }}</p>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- ここから下を変更してください -->
    <div class="row justify-content-center">
        <div class="col-md-8 mb-3">
            <ul class="list-group">
                @forelse ($comments as $comment)
                    <li class="list-group-item">
                        <div class="py-3 w-100 d-flex">
                            <img src="{{ asset('storage/profile_image/' .$comment->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                            <div class="ml-2 d-flex flex-column">
                                <p class="mb-0">{{ $comment->user->name }}</p>
                                <a href="{{ url('users/' .$comment->user->id) }}" class="text-secondary">{{ $comment->user->screen_name }}</a>
                            </div>
                            <div class="d-flex justify-content-end flex-grow-1">
                                <p class="mb-0 text-secondary">{{ $comment->created_at->format('Y-m-d H:i') }}</p>
                            </div>
                        </div>
                        <div class="py-3">
                            {!! nl2br(e($comment->text)) !!}
                        </div>
                    </li>
                @empty
                    <li class="list-group-item">
                        <p class="mb-0 text-secondary">コメントはまだありません</p>
                    </li>
                @endforelse
                <li class="list-group-item">
                    <div class="py-3">
                        <form method="POST" action="{{ route('comments.store') }}">
                            @csrf

                            <div class="form-group row mb-0">
                                <div class="col-md-12 p-3 w-100 d-flex">
                                    <img src="{{ asset('storage/profile_image/' .$user->profile_image) }}" class="rounded-circle" width="50" height="50">
                                    <div class="ml-2 d-flex flex-column">
                                        <p class="mb-0">{{ $user->name }}</p>
                                        <a href="{{ url('users/' .$user->id) }}" class="text-secondary">{{ $user->screen_name }}</a>
                                    </div>
                                </div>
                                <div class="col-md-12">
                                    <input type="hidden" name="tweet_id" value="{{ $tweet->id }}">
                                    <textarea class="form-control @error('text') is-invalid @enderror" name="text" required autocomplete="text" rows="4">{{ old('text') }}</textarea>

                                    @error('text')
                                        <span class="invalid-feedback" role="alert">
                                            <strong>{{ $message }}</strong>
                                        </span>
                                    @enderror
                                </div>
                            </div>

                            <div class="form-group row mb-0">
                                <div class="col-md-12 text-right">
                                    <p class="mb-4 text-danger">140文字以内</p>
                                    <button type="submit" class="btn btn-primary">
                                        ツイートする
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </li>
            </ul>
        </div>
    </div>
</div>
@endsection

これでコメントが付いていないときはコメントはまだありません。と表示されて、
ログインしてるユーザがコメント出来る様になっていると思います。

スクリーンショット 2019-10-02 22.46.55.png

投稿すると以下のようにちゃんとコメントが投稿されているのが確認できていると思います。

スクリーンショット 2019-10-02 22.48.31.png

これでコメント機能の実装は終わりです。

いいね機能

これでとりあえず最後の機能となります。こういうのも欲しいみたいな意見は聞かない

いいね機能では一人につき1いいね。
まだいいねを付けていない投稿に対してはいいねを保存。
逆に既にいいねを付けているツイートに対して再度いいねを押すと削除するという仕様にします。

ルーティング

さてお決まりのルーティングを設定していきましょう。
いいね機能では保存のstoreと削除のdestroyを設定しておきます。

routes/web.php
// ログイン状態
Route::group(['middleware' => 'auth'], function() {

    // 省略

    // いいね関連
    Route::resource('favorites', 'FavoritesController', ['only' => ['store', 'destroy']]);
});

Model

app/Models/Favorite.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Favorite extends Model
{
    public $timestamps = false;

    // いいねしているかどうかの判定処理
    public function isFavorite(Int $user_id, Int $tweet_id) 
    {
        return (boolean) $this->where('user_id', $user_id)->where('tweet_id', $tweet_id)->first();
    }

    public function storeFavorite(Int $user_id, Int $tweet_id)
    {
        $this->user_id = $user_id;
        $this->tweet_id = $tweet_id;
        $this->save();

        return;
    }

    public function destroyFavorite(Int $favorite_id)
    {
        return $this->where('id', $favorite_id)->delete();
    }
}

isFavoriteという見慣れないメソッドがありますね。
これはいいねを押した際にツイートに対して既にいいね済みであればfalse、逆に存在しなければtrue
これで正しいデータが飛んできたかどうかを判定しています。

Controller

またまたFavoritesController.phpファイルを--resourceで作成しましょう。

php artisan make:controller FavoritesController --resource

いいね機能の場合、見ている全ツイートに対していいね済かの判定があり同じViewファイルにstoredestroyactionが異なったformが混在した形で渡されます。
そのためログインしているユーザがツイートに対していいねをしているかの判定はフロントで行い、その判定の分岐でstoredestroyどちらかのデータがControllerに渡されます。

app/Http/Controllers/FavoritesController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Favorite;

class FavoritesController extends Controller
{
    public function store(Request $request, Favorite $favorite)
    {
        $user = auth()->user();
        $tweet_id = $request->tweet_id;
        $is_favorite = $favorite->isFavorite($user->id, $tweet_id);

        if(!$is_favorite) {
            $favorite->storeFavorite($user->id, $tweet_id);
            return back();
        }
        return back();
    }

    public function destroy(Favorite $favorite)
    {
        $user_id = $favorite->user_id;
        $tweet_id = $favorite->tweet_id;
        $favorite_id = $favorite->id;
        $is_favorite = $favorite->isFavorite($user_id, $tweet_id);

        if($is_favorite) {
            $favorite->destroyFavorite($favorite_id);
            return back();
        }
        return back();
    }
}

View

いいね機能もコメント機能同様にViewが存在しないので、こちらもtweetsに書いていきます。
コメントとは違ってツイート一覧画面とツイート詳細画面さらにユーザ詳細画面3つあるので、一気に3つやっていきます。

全て<!-- ここから --><!-- ここまで -->の間の部分を上書きしてください。

ユーザ詳細画面(users/show.blade.php)

先ほど実装したコメントのリンクもついでに設定しておきます。

resources/views/users/show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8 mb-3">
            <div class="card">
                <div class="d-inline-flex">
                    <div class="p-3 d-flex flex-column">
                        <img src="{{ asset('storage/profile_image/' .$user->profile_image) }}" class="rounded-circle" width="100" height="100">
                        <div class="mt-3 d-flex flex-column">
                            <h4 class="mb-0 font-weight-bold">{{ $user->name }}</h4>
                            <span class="text-secondary">{{ $user->screen_name }}</span>
                        </div>
                    </div>
                    <div class="p-3 d-flex flex-column justify-content-between">
                        <div class="d-flex">
                            <div>
                                @if ($user->id === Auth::user()->id)
                                    <a href="{{ url('users/' .$user->id .'/edit') }}" class="btn btn-primary">プロフィールを編集する</a>
                                @else
                                    @if ($is_following)
                                        <form action="{{ route('unfollow', ['id' => $user->id]) }}" method="POST" class="mb-2">
                                            {{ csrf_field() }}
                                            {{ method_field('DELETE') }}

                                            <button type="submit" class="btn btn-danger">フォロー解除</button>
                                        </form>
                                    @else
                                        <form action="{{ route('follow', ['id' => $user->id]) }}" method="POST" class="mb-2">
                                            {{ csrf_field() }}

                                            <button type="submit" class="btn btn-primary">フォローする</button>
                                        </form>
                                    @endif

                                    @if ($is_followed)
                                        <span class="mt-2 px-1 bg-secondary text-light">フォローされています</span>
                                    @endif
                                @endif
                            </div>
                        </div>
                        <div class="d-flex justify-content-end">
                            <div class="p-2 d-flex flex-column align-items-center">
                                <p class="font-weight-bold">ツイート数</p>
                                <span>{{ $tweet_count }}</span>
                            </div>
                            <div class="p-2 d-flex flex-column align-items-center">
                                <p class="font-weight-bold">フォロー数</p>
                                <span>{{ $follow_count }}</span>
                            </div>
                            <div class="p-2 d-flex flex-column align-items-center">
                                <p class="font-weight-bold">フォロワー数</p>
                                <span>{{ $follower_count }}</span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        @if (isset($timelines))
            @foreach ($timelines as $timeline)
                <div class="col-md-8 mb-3">
                    <div class="card">
                        <div class="card-haeder p-3 w-100 d-flex">
                            <img src="{{ asset('storage/profile_image/' .$user->profile_image) }}" class="rounded-circle" width="50" height="50">
                            <div class="ml-2 d-flex flex-column flex-grow-1">
                                <p class="mb-0">{{ $timeline->user->name }}</p>
                                <a href="{{ url('users/' .$timeline->user->id) }}" class="text-secondary">{{ $timeline->user->screen_name }}</a>
                            </div>
                            <div class="d-flex justify-content-end flex-grow-1">
                                <p class="mb-0 text-secondary">{{ $timeline->created_at->format('Y-m-d H:i') }}</p>
                            </div>
                        </div>
                        <div class="card-body">
                            {{ $timeline->text }}
                        </div>
                        <div class="card-footer py-1 d-flex justify-content-end bg-white">
                            @if ($timeline->user->id === Auth::user()->id)
                                <div class="dropdown mr-3 d-flex align-items-center">
                                    <a href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                        <i class="fas fa-ellipsis-v fa-fw"></i>
                                    </a>
                                    <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                                        <form method="POST" action="{{ url('tweets/' .$timeline->id) }}" class="mb-0">
                                            @csrf
                                            @method('DELETE')

                                            <a href="{{ url('tweets/' .$timeline->id .'/edit') }}" class="dropdown-item">編集</a>
                                            <button type="submit" class="dropdown-item del-btn">削除</button>
                                        </form>
                                    </div>
                                </div>
                            @endif

                            <!-- ここから -->
                            <div class="mr-3 d-flex align-items-center">
                                <a href="{{ url('tweets/' .$timeline->id) }}"><i class="far fa-comment fa-fw"></i></a>
                                <p class="mb-0 text-secondary">{{ count($timeline->comments) }}</p>
                            </div>
                            <div class="d-flex align-items-center">
                                @if (!in_array(Auth::user()->id, array_column($timeline->favorites->toArray(), 'user_id'), TRUE))
                                    <form method="POST" action="{{ url('favorites/') }}" class="mb-0">
                                        @csrf

                                        <input type="hidden" name="tweet_id" value="{{ $timeline->id }}">
                                        <button type="submit" class="btn p-0 border-0 text-primary"><i class="far fa-heart fa-fw"></i></button>
                                    </form>
                                @else
                                    <form method="POST"action="{{ url('favorites/' .array_column($timeline->favorites->toArray(), 'id', 'user_id')[Auth::user()->id]) }}" class="mb-0">
                                        @csrf
                                        @method('DELETE')

                                        <button type="submit" class="btn p-0 border-0 text-danger"><i class="fas fa-heart fa-fw"></i></button>
                                    </form>
                                @endif
                                <p class="mb-0 text-secondary">{{ count($timeline->favorites) }}</p>
                            </div>
                            <!-- ここまで -->

                        </div>
                    </div>
                </div>
            @endforeach
        @endif
    </div>
    <div class="my-4 d-flex justify-content-center">
        {{ $timelines->links() }}
    </div>
</div>
@endsection

ツイート一覧画面(tweets/index.blade.php)

resources/views/tweets/index.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8 mb-3 text-right">
            <a href="{{ url('users') }}">ユーザ一覧 <i class="fas fa-users" class="fa-fw"></i> </a>
        </div>
        @if (isset($timelines))
            @foreach ($timelines as $timeline)
                <div class="col-md-8 mb-3">
                    <div class="card">
                        <div class="card-haeder p-3 w-100 d-flex">
                            <img src="{{ asset('storage/profile_image/' .$timeline->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                            <div class="ml-2 d-flex flex-column">
                                <p class="mb-0">{{ $timeline->user->name }}</p>
                                <a href="{{ url('users/' .$timeline->user->id) }}" class="text-secondary">{{ $timeline->user->screen_name }}</a>
                            </div>
                            <div class="d-flex justify-content-end flex-grow-1">
                                <p class="mb-0 text-secondary">{{ $timeline->created_at->format('Y-m-d H:i') }}</p>
                            </div>
                        </div>
                        <div class="card-body">
                            {!! nl2br(e($timeline->text)) !!}
                        </div>
                        <div class="card-footer py-1 d-flex justify-content-end bg-white">
                            @if ($timeline->user->id === Auth::user()->id)
                                <div class="dropdown mr-3 d-flex align-items-center">
                                    <a href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                        <i class="fas fa-ellipsis-v fa-fw"></i>
                                    </a>
                                    <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                                        <form method="POST" action="{{ url('tweets/' .$timeline->id) }}" class="mb-0">
                                            @csrf
                                            @method('DELETE')

                                            <a href="{{ url('tweets/' .$timeline->id .'/edit') }}" class="dropdown-item">編集</a>
                                            <button type="submit" class="dropdown-item del-btn">削除</button>
                                        </form>
                                    </div>
                                </div>
                            @endif
                            <div class="mr-3 d-flex align-items-center">
                                <a href="{{ url('tweets/' .$timeline->id) }}"><i class="far fa-comment fa-fw"></i></a>
                                <p class="mb-0 text-secondary">{{ count($timeline->comments) }}</p>
                            </div>

                            <!-- ここから -->
                            <div class="d-flex align-items-center">
                                @if (!in_array($user->id, array_column($timeline->favorites->toArray(), 'user_id'), TRUE))
                                    <form method="POST" action="{{ url('favorites/') }}" class="mb-0">
                                        @csrf

                                        <input type="hidden" name="tweet_id" value="{{ $timeline->id }}">
                                        <button type="submit" class="btn p-0 border-0 text-primary"><i class="far fa-heart fa-fw"></i></button>
                                    </form>
                                @else
                                    <form method="POST" action="{{ url('favorites/' .array_column($timeline->favorites->toArray(), 'id', 'user_id')[$user->id]) }}" class="mb-0">
                                        @csrf
                                        @method('DELETE')

                                        <button type="submit" class="btn p-0 border-0 text-danger"><i class="fas fa-heart fa-fw"></i></button>
                                    </form>
                                @endif
                                <p class="mb-0 text-secondary">{{ count($timeline->favorites) }}</p>
                            </div>
                            <!-- ここまで -->

                        </div>
                    </div>
                </div>
            @endforeach
        @endif
    </div>
    <div class="my-4 d-flex justify-content-center">
        {{ $timelines->links() }}
    </div>
</div>
@endsection

ツイート詳細画面(tweets/show.blade.php)

resources/views/tweets/show.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center mb-5">
        <div class="col-md-8 mb-3">
            <div class="card">
                <div class="card-haeder p-3 w-100 d-flex">
                    <img src="{{ asset('storage/profile_image/' .$tweet->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                    <div class="ml-2 d-flex flex-column">
                        <p class="mb-0">{{ $tweet->user->name }}</p>
                        <a href="{{ url('users/' .$tweet->user->id) }}" class="text-secondary">{{ $tweet->user->screen_name }}</a>
                    </div>
                    <div class="d-flex justify-content-end flex-grow-1">
                        <p class="mb-0 text-secondary">{{ $tweet->created_at->format('Y-m-d H:i') }}</p>
                    </div>
                </div>
                <div class="card-body">
                    {!! nl2br(e($tweet->text)) !!}
                </div>
                <div class="card-footer py-1 d-flex justify-content-end bg-white">
                    @if ($tweet->user->id === Auth::user()->id)
                        <div class="dropdown mr-3 d-flex align-items-center">
                            <a href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                <i class="fas fa-ellipsis-v fa-fw"></i>
                            </a>
                            <div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                                <form method="POST" action="{{ url('tweets/' .$tweet->id) }}" class="mb-0">
                                    @csrf
                                    @method('DELETE')

                                    <a href="{{ url('tweets/' .$tweet->id .'/edit') }}" class="dropdown-item">編集</a>
                                    <button type="submit" class="dropdown-item del-btn">削除</button>
                                </form>
                            </div>
                        </div>
                    @endif
                    <div class="mr-3 d-flex align-items-center">
                        <a href="{{ url('tweets/' .$tweet->id) }}"><i class="far fa-comment fa-fw"></i></a>
                        <p class="mb-0 text-secondary">{{ count($tweet->comments) }}</p>
                    </div>

                    <!-- ここから -->
                    <div class="d-flex align-items-center">
                        @if (!in_array($user->id, array_column($tweet->favorites->toArray(), 'user_id'), TRUE))
                            <form method="POST" action="{{ url('favorites/') }}" class="mb-0">
                                @csrf

                                <input type="hidden" name="tweet_id" value="{{ $tweet->id }}">
                                <button type="submit" class="btn p-0 border-0 text-primary"><i class="far fa-heart fa-fw"></i></button>
                            </form>
                        @else
                            <form method="POST" action="{{ url('favorites/' .array_column($tweet->favorites->toArray(), 'id', 'user_id')[$user->id]) }}" class="mb-0">
                                @csrf
                                @method('DELETE')

                                <button type="submit" class="btn p-0 border-0 text-danger"><i class="fas fa-heart fa-fw"></i></button>
                            </form>
                        @endif
                        <p class="mb-0 text-secondary">{{ count($tweet->favorites) }}</p>
                    </div>
                    <!-- ここまで -->

                </div>
            </div>
        </div>
    </div>

    <div class="row justify-content-center">
        <div class="col-md-8 mb-3">
            <ul class="list-group">
                @forelse ($comments as $comment)
                    <li class="list-group-item">
                        <div class="py-3 w-100 d-flex">
                            <img src="{{ asset('storage/profile_image/' .$comment->user->profile_image) }}" class="rounded-circle" width="50" height="50">
                            <div class="ml-2 d-flex flex-column">
                                <p class="mb-0">{{ $comment->user->name }}</p>
                                <a href="{{ url('users/' .$comment->user->id) }}" class="text-secondary">{{ $comment->user->screen_name }}</a>
                            </div>
                            <div class="d-flex justify-content-end flex-grow-1">
                                <p class="mb-0 text-secondary">{{ $comment->created_at->format('Y-m-d H:i') }}</p>
                            </div>
                        </div>
                        <div class="py-3">
                            {!! nl2br(e($comment->text)) !!}
                        </div>
                    </li>
                @empty
                    <li class="list-group-item">
                        <p class="mb-0 text-secondary">コメントはまだありません</p>
                    </li>
                @endforelse
                <li class="list-group-item">
                    <div class="py-3">
                        <form method="POST" action="{{ route('comments.store') }}">
                            @csrf

                            <div class="form-group row mb-0">
                                <div class="col-md-12 p-3 w-100 d-flex">
                                    <img src="{{ asset('storage/profile_image/' .$user->profile_image) }}" class="rounded-circle" width="50" height="50">
                                    <div class="ml-2 d-flex flex-column">
                                        <p class="mb-0">{{ $user->name }}</p>
                                        <a href="{{ url('users/' .$user->id) }}" class="text-secondary">{{ $user->screen_name }}</a>
                                    </div>
                                </div>
                                <div class="col-md-12">
                                    <input type="hidden" name="tweet_id" value="{{ $tweet->id }}">
                                    <textarea class="form-control @error('text') is-invalid @enderror" name="text" required autocomplete="text" rows="4">{{ old('text') }}</textarea>

                                    @error('text')
                                        <span class="invalid-feedback" role="alert">
                                            <strong>{{ $message }}</strong>
                                        </span>
                                    @enderror
                                </div>
                            </div>

                            <div class="form-group row mb-0">
                                <div class="col-md-12 text-right">
                                    <p class="mb-4 text-danger">140文字以内</p>
                                    <button type="submit" class="btn btn-primary">
                                        ツイートする
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div>
                </li>
            </ul>
        </div>
    </div>
</div>
@endsection

これで各画面でいいねを押してみる

ユーザ詳細画面

スクリーンショット 2019-10-02 23.37.20.png

ツイート詳細画面では逆にいいねを外してみる

ツイート詳細画面

スクリーンショット 2019-10-02 23.34.08.png

一覧画面ではもう一度いいねをしてみて動いてるか確認する。

ツイート一覧画面

スクリーンショット 2019-10-02 23.33.23.png

完成!!?‍??‍??‍?

Congratulations!

これで【全6回】Laravel5.8でTwitterっぽいSNSツールを作るは終了です。
なんとなくLaravelの取っ付きやすさは伝わりましたか?
これを機にLaravelを使いたくなったという方がいれば嬉しいです!!?‍?
お疲れ様でした!!

絶対どこがで間違いがあるはずなので、都度修正していきます。
訂正箇所があれば教えていただければ幸いです?

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

【全6回】Laravel5.8でTwitterっぽいSNSツールを作る(第5回ツイートのCRUD機能 編集と削除)

Laravelで始めるTwitter風(Twitterクローン)のSNSツール開発チュートリアル

概要

スクールとかの課題だったりLaravelを初めてみたいけど何を作ろうって迷ってる人向けによくあるTwitter風のWEBサイトを作ってみます。

前回

第4回ツイートのCRUD機能の続きを追加していきます。
今回はツイートの編集(Update)と削除(Delete)をやっていきます。

前提

  • PHPをある程度理解している
  • Homesteadをインストールしている
  • MVC構造をある程度理解している

環境

  • Mac
  • Homestead
  • Laravel 5.8

リダイレクト先

ついでに現在ログインすると/homeに飛ぶようになっているのでこちらも/tweetsにリダイレクトするように設定しましょう(忘れてた)

  • 対象のファイル名
    • LoginController.php
    • RegisterController.php
    • ResetPasswordController.php
    • VerificationController.php
    • RedirectIfAuthenticated.php

上記のファイルから/homeの部分を全て/tweetsに変更してください。

そうするとログイン後も/tweetsにリダイレクトするようになります!

Update(ツイート編集機能)

ではツイート内容の編集機能を実装していきましょう。

Update(ツイート編集画面)

では次はCRUDのUpdateをやっていきましょう!
まずはツイート投稿と同じく編集画面から作っていきます。

Model

$user_id$tweet_idに値に一致するツイートを取得します。

app/Models/Tweet.php
    public function getEditTweet(Int $user_id, Int $tweet_id)
    {
        return $this->where('user_id', $user_id)->where('id', $tweet_id)->first();
    }

Controller

編集するツイートを先ほどのgetEditTweet$tweet_idを渡してその結果をViewに渡す処理をします。

app/Http/Controllers/TweetsController.php
    public function edit(Tweet $tweet)
    {
        $user = auth()->user();
        $tweets = $tweet->getEditTweet($user->id, $tweet->id);

        if (!isset($tweets)) {
            return redirect('tweets');
        }

        return view('tweets.edit', [
            'user'   => $user,
            'tweets' => $tweets
        ]);
    }

View

resources/views/tweets/edit.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">Update</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('tweets.update', ['tweets' => $tweets]) }}">
                        @csrf
                        @method('PUT')

                        <div class="form-group row mb-0">
                            <div class="col-md-12 p-3 w-100 d-flex">
                                <img src="{{ asset('storage/profile_image/' .$user->profile_image) }}" class="rounded-circle" width="50" height="50">
                                <div class="ml-2 d-flex flex-column">
                                    <p class="mb-0">{{ $user->name }}</p>
                                    <a href="{{ url('users/' .$user->id) }}" class="text-secondary">{{ $user->screen_name }}</a>
                                </div>
                            </div>
                            <div class="col-md-12">
                                <textarea class="form-control @error('text') is-invalid @enderror" name="text" required autocomplete="text" rows="4">{{ old('text') ? : $tweets->text }}</textarea>

                                @error('text')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-12 text-right">
                                <p class="mb-4 text-danger">140文字以内</p>
                                <button type="submit" class="btn btn-primary">
                                    ツイートする
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

ツイート編集画面

ツイート一覧ページから自身のツイートのみ表示されるボタン「・・・←これを縦にしたアイコン 笑」を押すと編集ボタンが表示されるのでそれを押すとこのページに行けます!

スクリーンショット 2019-09-19 2.05.48.png

Update(ツイート編集機能)

では先ほどの編集画面から実際に編集した内容で更新する処理を書いていきましょう!

Model

この辺はただ上書きしているだけなので、特に触れません。

もしわからない人がいれば第3回のUser情報を更新するときと流れは一緒なのでもう一度見てみてください。

app/Models/Tweet.php
    public function tweetUpdate(Int $tweet_id, Array $data)
    {
        $this->id = $tweet_id;
        $this->text = $data['text'];
        $this->update();

        return;
    }

Controller

こちらも先ほど作成したstoreとほとんど同じなので説明は省きます。

app/Http/Controllers/TweetsController.php
    public function update(Request $request, Tweet $tweet)
    {
        $data = $request->all();
        $validator = Validator::make($data, [
            'text' => ['required', 'string', 'max:140']
        ]);

        $validator->validate();
        $tweet->tweetUpdate($tweet->id, $data);

        return redirect('tweets');
    }

これで編集ができるようになっていると思います!

Delete(ツイート削除)

次はツイートの削除を行ってみようと思います?‍?

Delete(ツイート削除機能)

Model

$user_id$tweet_idに一致したツイートを削除します。

app/Models/Tweet.php
    public function tweetDestroy(Int $user_id, Int $tweet_id)
    {
        return $this->where('user_id', $user_id)->where('id', $tweet_id)->delete();
    }

Controller

$user_id$tweet_idを先ほど作成したtweetDestroy()メソッドに渡しています。

app/Http/Controllers/TweetsController.php
    public function destroy(Tweet $tweet)
    {
        $user = auth()->user();
        $tweet->tweetDestroy($user->id, $tweet->id);

        return back();
    }

これで削除機能を実装できましたので、先ほどの編集画面に遷移する方法と同様に削除するボタンがあるので試してみてください。

振り返り(ModelとControllerファイルの全体)

今回第4,5回とModelとControllerの行き来が多かったのでとりあえず全体も載せておきます!
ここまで問題なく動いたよって人はスルーしてください。

Model

app/Models/Tweet.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\softDeletes;

class Tweet extends Model
{
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'text'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function favorites()
    {
        return $this->hasMany(Favorite::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function getUserTimeLine(Int $user_id)
    {
        return $this->where('user_id', $user_id)->orderBy('created_at', 'DESC')->paginate(50);
    }

    public function getTweetCount(Int $user_id)
    {
        return $this->where('user_id', $user_id)->count();
    }

    // 詳細画面
    public function getTweet(Int $tweet_id)
    {
        return $this->with('user')->where('id', $tweet_id)->first();
    }

    // 一覧画面
    public function getTimeLines(Int $user_id, Array $follow_ids)
    {
        $follow_ids[] = $user_id;
        return $this->whereIn('user_id', $follow_ids)->orderBy('created_at', 'DESC')->paginate(50);
    }

    public function tweetStore(Int $user_id, Array $data)
    {
        $this->user_id = $user_id;
        $this->text = $data['text'];
        $this->save();

        return;
    }

    public function getEditTweet(Int $user_id, Int $tweet_id)
    {
        return $this->where('user_id', $user_id)->where('id', $tweet_id)->first();
    }

    public function tweetUpdate(Int $tweet_id, Array $data)
    {
        $this->id = $tweet_id;
        $this->text = $data['text'];
        $this->update();

        return;
    }

    public function tweetDestroy(Int $user_id, Int $tweet_id)
    {
        return $this->where('user_id', $user_id)->where('id', $tweet_id)->delete();
    }
}

Controller

app/Http/Controllers/TweetsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Models\Tweet;
use App\Models\Comment;
use App\Models\Follower;

class TweetsController extends Controller
{
    public function index(Tweet $tweet, Follower $follower)
    {
        $user = auth()->user();
        $follow_ids = $follower->followingIds($user->id);
        $following_ids = $follow_ids->pluck('followed_id')->toArray();

        $timelines = $tweet->getTimelines($user->id, $following_ids);

        return view('tweets.index', [
            'user'      => $user,
            'timelines' => $timelines
        ]);
    }

    public function create()
    {
        $user = auth()->user();

        return view('tweets.create', [
            'user' => $user
        ]);
    }

    public function store(Request $request, Tweet $tweet)
    {
        $user = auth()->user();
        $data = $request->all();

        $validator = Validator::make($data, [
            'text' => ['required', 'string', 'max:140']
        ]);
        $validator->validate();

        $tweet->tweetStore($user->id, $data);

        return redirect('tweets');
    }

    public function show(Tweet $tweet, Comment $comment)
    {
        $user = auth()->user();
        $tweet = $tweet->getTweet($tweet->id);
        $comments = $comment->getComments($tweet->id);

        return view('tweets.show', [
            'user'     => $user,
            'tweet'    => $tweet,
            'comments' => $comments
        ]);
    }

    public function edit(Tweet $tweet)
    {
        $user = auth()->user();
        $tweets = $tweet->getEditTweet($user->id, $tweet->id);

        if (!isset($tweets)) {
            return redirect('tweets');
        }

        return view('tweets.edit', [
            'user'   => $user,
            'tweets' => $tweets
        ]);
    }

    public function update(Request $request, Tweet $tweet)
    {
        $data = $request->all();
        $validator = Validator::make($data, [
            'text' => ['required', 'string', 'max:140']
        ]);

        $validator->validate();
        $tweet->tweetUpdate($tweet->id, $data);

        return redirect('tweets');
    }

    public function destroy(Tweet $tweet)
    {
        $user = auth()->user();
        $tweet->tweetDestroy($user->id, $tweet->id);

        return back();
    }
}

以上です。

次回 -> 【全6回】Laravel5.8でTwitterっぽいSNSツールを作る(第6回コメントといいね機能)

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

PHP 7.3からdate_create_from_format("u")の挙動が変わった

誰も使ってなさそうなPHP関数date_create_from_format()の挙動が変わった話を紹介します。

誰も困らない内容だと思いますが、気付いてしまったので記録として残しておきます。

date_create_from_format関数とは

date_create_from_format()は任意のフォーマットの日付文字列を元にDateTimeインスタンスを返す関数です。

<?php
$dt = date_create_from_format("Y年m月d日 H時i分s秒", "2019年1月2日 12時34分56秒");
var_dump($dt->format("Y-m-d H:i:s"));

このように第一引数でフォーマットを指定し、第二引数に日付文字列を渡して利用します。上記コードを実行すると次の結果を得ます。

string(25) "2019-01-02 12:34:56"

指定されていない部分の扱いについて

date_create_from_formatの引数で年月日時分秒のうち一部しか与えられなかったような場合、指定されなかった部分には現在の時刻が採用される、とPHPマニュアルには書いてあります。

format に文字 ! が含まれない場合は、作成した時刻値のうち format で指定されていない部分を 現在のシステム時刻で初期化します。

これは次のように確認することができます。

<?php
$dt = date_create_from_format("H時i分s秒", "12時34分56秒");
var_dump($dt->format("Y-m-d H:i:s"));

これを2019-10-02に実行したところ次の結果が得られました。

string(25) "2019-10-02 12:34:56"

指定されなかった年月日が現在の値で埋められており、マニュアルの記述通りの挙動であることがわかります。

しかし、マニュアルの記述は不正確で、特定の条件の時にゼロが入ることがあります。

<?php
$dt = date_create_from_format("s秒", "56秒");
var_dump($dt->format("Y-m-d H:i:s"));

これを実行すると次のようになります。

string(25) "2019-10-02 00:00:56"

秒しか指定しなかった場合に、時と分は現在時刻で埋められず、必ず0になります。これ以外の組み合わせでも、時分秒のうち1つまたは2つが指定された場合、指定されなかった部分は0になります(これは文書化されていない情報だと思います)。

PHP 7.3でフォーマット文字列"u"の扱いが変わった

ようやく本題です。PHP 7.3からフォーマット文字列で"u"(マイクロ秒)を指定した場合の挙動が変更されています。

<?php
$dt = date_create_from_format("Y-m-d u", "2019-01-02 654321");
var_dump($dt->format("Y-m-d H:i:s.u"));

これをPHP 7.2以前で実行すると、指定されていない時分秒の全てが現在時刻になります。

string(26) "2019-01-02 13:05:46.654321"

しかし、PHP 7.3では次のようになります。

string(26) "2019-01-02 00:00:00.654321"

マイクロ秒が指定された場合も、時分秒のうち指定されていない部分が0になるようになりました。

まとめ

もともと文書化されていない挙動がPHPの特定バージョンから変わったよ、というお話でした。

補足

本件バグレポ上がってるけど瞬時にDocumentation Problemに格下げになってますね。

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

Goの練習 PHPerが業務用Webアプリケーションでありそうな処理をGoと速度比較してみる

はじめに

PHPerとして仕事を始めて6年半(2019年現在)。様々な現場で業務用Webアプリケーションの構築に携わってきました。
Webアプリケーションであれば業務で使うような大体の機能はPHPで実現できるのですが、1システムに1機能はPHPだと物足りないというか、痒い所に手が届かないなと思う箇所はどうしても出てきます。

顕著にそれを思うのは大量データの処理などのパフォーマンスを発揮したい機能です。
PHP7の登場でPHP5と比較して格段にパフォーマンスが向上しました。ですがそれでももっと速く処理したいと思う場面がしばしばあります。

そこで自身のスキルの幅を広げることも含めて、スクリプト言語のように記述出来てハイパフォーマンスが期待できるGoを学んでみようと思いました。

本記事の概要

経験上Webアプリケーションでありそう、かつ処理が重そうな機能を考えた結果、CSVアップロード/ダウンロード機能が思いつきました。
今回はフロント部分は省略して、コマンドラインでCSVをDBに保存する機能とDBからCSVを作成する機能をGoとPHPで作成し、そのパフォーマンスの差を検証します。
諸先輩方が通ってきた道かと思いますが、自学のため。

機能の概要

アップロード

  • あらかじめ用意したCSVを読み込み、DBにアップロードする。
  • 指定した件数をひとかたまりとして処理する
  • 新規追加フラグを設け、これが1の場合は新規追加とみなしDBとの重複チェックを行う。

使用したCSVは国交省提供の位置参照情報ダウンロードサービスから全自治体のデータをダウンロードし加工したものになります。
およそ21万件ほど。

UPSERTをするようにしたので結局重複チェックは意味をなさないのですが、それっぽく作るため入れてみました。

ダウンロード

  • DBからデータを全量取得し、CSVに出力する。
  • 指定した件数をひとかたまりとして処理する

こちらの方が純粋なパフォーマンス比較ができそうです。

両方の機能でGoでは並行処理での処理を指定可能。並行処理では2スレッドで処理します。

やらないこと

細かいことは抜きにする方針にします。

  • フロント部分(GUI)の作成
  • 細かいバリデーション
    • アップロード用CSVの存在チェック
    • 形式チェック
  • SQLインジェクション対策

等々

ハマったら容赦なくOSSライブラリを使おうと思います。
逆に言えばハマるまでは標準パッケージで頑張ります。

実行環境等

サーバ
Amazon Linux 2 AMI (HVM), SSD Volume Type
t2.medium(CPU 2コア メモリ 4GB)

言語
go v1.13
php v7.2.22

DB
MySQL 5.7

AmamzonLinuxの上にそれぞれのアプリケーションとDBのDockerコンテナを建てて実行しています。

実行結果

ソースは→ GitHub

GNU版timeコマンドを利用しています。Goはコンパイル済みのファイルを実行。

まずはアップロードから。重複エラーがないパターン。

言語 real user sys 消費メモリ
Go 7.91s 1.00s 0.05s 3.62MB
Go(並行処理) 4.18s 1.00s 0.09s 5.65MB
PHP 7.00s 1.61s 0.11s 6.32MB

全件重複エラーするパターン。

言語 real user sys 消費メモリ
Go 8.46s 1.20s 0.30s 3.62MB
Go(並行処理) 5.27s 1.23s 0.23s 6.18MB
PHP 8.91s 1.90s 0.15s 16.99MB

次はダウンロード

言語 real user sys 消費メモリ
Go 0.92s 0.74s 0.05s 2.42MB
Go(並行処理) 0.70s 0.82s 0.04s 3.97MB
PHP 1.07s 0.54s 0.24s 7.47MB

グラフにしてみます。


実際に処理にかかった時間。意外とPHPと差はない、どころか逐次実行の場合はGoがPHPより1秒ほどかかっています。並行処理の場合は流石に速いですね。


CPUが処理をした時間。並行処理と逐次処理の間に大きな差はなし。アップロードにおいてはPHPの方が占有時間が長いようです。


OSがシステムコールに使った時間。正直なところ語れるほど詳しくないのですが場合によりけりといった印象です。


消費メモリ。Goの方が軒並み少ないですね。

所感

測定結果に関して

  • 当初想定していたよりもGoとPHPの差はあまりありませんでした。実際の処理時間に焦点を当てると場合によってはPHPの方が速かったりということもあり、驚きました。
    私自身がGo初学者というのもあり、パフォーマンスチューニングができていない可能性も高いです。
    ただし並行処理になるとやはりというべきか結構な時間短縮になりますね。

  • メモリ消費量はGoの方がパフォーマンスが良さそうなので、同じ処理でもサーバの負荷を抑えられるなどのメリットはありそうです。
    ここはスクリプト言語とコンパイル形式の言語の違いもありそうですが。

  • データ量が増えたりするとまた差がでたりするのでしょうか。

実装に関して

  • Scan関数の引数で取得したカラムを一つ一つ指定して変数に入れてあげないといけないのは少し面倒。
  • DBから取得したデータをうまいことCSVに書き出す方法を見つけるのにハマったので結局gocsvに頼りました。
  • エラーハンドリングはもう少ししっかりやった方がいいかなと思いました。特に問答無用のexitは実務レベルではあまりやらないと思います。
  • アップロードで指定した件数をひとかたまりとしてに重複チェックと投入をしていますが、1件でも重複チェックに引っかかったかたまりは投入されず、引っかからなかったかたまりは投入されるというよくわからない状況になっています。業務では引っかからなかったデータだけ投入するとか、エラーが1件でもあったら投入しないという仕様にするのが妥当だと思います。完成してから気づきました… 今回は重複チェック意味ないのでご容赦を。
  • 今回Goの実装に費やした時間は余暇時間を使って環境構築したりハマったり調べたりを繰り返し、大体1週間ぐらいでできました。時間がかかるのは想定していましたが、それでも結構時間がかかった印象です。本人のスキルが多分にありそうですが、毛色の違う言語から来たときの学習コストがなんとなくわかりました。PHPの方は1時間もかからずにできました。

なんとなく触ることはできたので、次は何か開発してみたいと思います。いつできるかはわかりませんが。

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

Laravelで実装したAPIがOpenAPIで記述された仕様に準拠しているかテストする

はじめに

APIを開発する上で、多くの場合、仕様書も作成するかと思いますが、どのように作成しているでしょうか?
この記事では、OpenAPI形式で記述されたAPI仕様があって、その仕様にAPIの実装が準拠しているかテストする方法を紹介します。

tl;dr

OpenAPI v3とは(Swagger v2との違い)

詳しくは後述の参考リンクを参照してほしいのですが、誤解を恐れずに要点をまとめると以下のようになります。

  • APIを定義する仕様として、2010年にSwagger 1.0がリリースされる
  • Swaggerは、Open API Initiativeに寄贈され、2017-07-26にOpen API 3.0がリリースされる
  • Open API 3.0Swagger 2.0から大幅な変更が加えられており、互換性はない

参考リンク

OpenAPI v3に対応した検証ライブラリは多くない(2019年10月1日現在)

Swagger v2に対応したPHP製の検証ライブラリはいくつかあります。
しかし、いずれも、OpenAPI v3には未対応です。

WakeOnWeb/swagger

OpenAPI v3 support · Issue #16 · WakeOnWeb/swaggerはいまだにOpenのままです。

nabbar/SwaggerValidator-PHP

README.mdを見ると、swagger / openapi version 3.0 (release >= 2.0)とあるので、v2.0以上だったらOpen API 3.0に対応しているのかと思いきや、最新のバージョンは1.3.2です。
対応予定だけ書いて力尽きたようです。

OpenAPI v3形式のファイルをJSON Schemaに変換すれば、検証はできる

laravel-petstore-apiの最初のコミットでもこの方法を使っているのですが、OpenAPI v3形式のファイルをJSON Schemaに変換し、RequestおよびResponseの検証はできます。

ポイントは以下のとおりです。

2019年9月 thephpleague/openapi-psr7-validatorがリリースされる

この記事を書いている途中で気付いてしまったのですが、thephpleague/openapi-psr7-validatorという何とも期待の持てるライブラリがあったので、試してみました。
結果、期待どおり動いたので、それまでのJSON Schemaに変換してから検証する実装、関連ライブラリを全部捨ててthephpleague/openapi-psr7-validatorで実装し直しました。

実装

実装は下記のようになりました。

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\TestResponse;
use Nyholm\Psr7\Factory\Psr17Factory;
use OpenAPIValidation\PSR7\Exception\ValidationFailed;
use OpenAPIValidation\PSR7\OperationAddress;
use OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Tests\TestCase;

/**
 * Trait AssertResponseCompliantForSwaggerApiSpec
 * @package Tests\Feature
 *
 * @mixin TestCase
 */
trait OpenApiSpecAssertions
{
    /**
     * @param TestResponse $testResponse
     * @param string $method
     * @param string $path
     */
    protected function assertResponseCompliantForOpenApiSpec(
        TestResponse $testResponse,
        string $method,
        string $path
    ) {
        $validator = (new ValidatorBuilder())
            ->fromYamlFile(__DIR__. '/../ApiSpec/petstore-expanded.yaml')
            ->getResponseValidator()
        ;

        $operation = new OperationAddress($path, strtolower($method)) ;

        $psr17Factory = new Psr17Factory();
        $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
        $psrResponse = $psrHttpFactory->createResponse($testResponse->baseResponse);

        try {
            $validator->validate($operation, $psrResponse);
        } catch (ValidationFailed $validationFailed) {
            self::fail($validationFailed->getPrevious()->getMessage());
        }
    }

    /**
     * @param array $requestBody
     * @param string $method
     * @param string $uri
     */
    protected function assertRequestCompliantForOpenApiSpec(
        array $requestBody,
        string $method,
        string $uri
    ) {
        $validator = (new ValidatorBuilder())
            ->fromYamlFile(__DIR__. '/../ApiSpec/petstore-expanded.yaml')
            ->getRequestValidator()
        ;

        $psr17Factory = new Psr17Factory();
        $json = json_encode($requestBody, JSON_UNESCAPED_UNICODE);
        $stream = $psr17Factory->createStream($json);
        $stream->rewind();
        $request = $psr17Factory->createRequest(strtolower($method), $uri)
            ->withBody($stream)
            ->withHeader('Content-Type', 'application/json')
        ;

        try {
            $validator->validate($request);
        } catch (ValidationFailed $validationFailed) {
            self::fail($validationFailed->getPrevious()->getMessage());
        }
    }
}

ポイント

  • tests/ApiSpec/petstore-expanded.yamlOpenAPI v3形式のファイルを配置
  • OpenAPIValidation\PSR7\ValidatorBuilderfromYamlFilevalidatorを生成
  • PSR-7に準拠したRequest/Responseでなければいけないので、変換する必要がある
  • PSR-7形式への変換については、The PSR-7 Bridge (Symfony Docs)を参照
  • 仕様に違反した場合は、OpenAPIValidation\PSR7\Exception\ValidationFailedthrowされる
  • 今回はテストを失敗させるようにした

API仕様に違反すると

API仕様を変更して、テストを失敗させてみます。

任意項目だったNewPetおよびPettagというpropertyを必須(required)にして、テストを実行します。

openapi: "3.0.0"
## 中略 ##

components:
  schemas:
    Pet:
      allOf:
        - $ref: '#/components/schemas/NewPet'
        - type: object
          required:
          - id
          properties:
            id:
              type: integer
              format: int64

    NewPet:
      type: object
      required:
        - name
        - tag ## この行を追加
      properties:
        name:
          type: string
        tag:
          type: string

Keyword validation failed: Required property 'tag' must be present in the object

というメッセージとともに、テストが失敗します。

$ vendor/bin/phpunit
PHPUnit 8.3.5 by Sebastian Bergmann and contributors.

..FFF...                                                            8 / 8 (100%)

Time: 405 ms, Memory: 24.00 MB

There were 3 failures:

1) Tests\Feature\Pet\IndexTest::indexSuccess
Keyword validation failed: Required property 'tag' must be present in the object

/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:45
/path/to/laravel-petstore-api/tests/Feature/Pet/IndexTest.php:23

2) Tests\Feature\Pet\ShowTest::showSuccess
Keyword validation failed: Required property 'tag' must be present in the object

/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:45
/path/to/laravel-petstore-api/tests/Feature/Pet/ShowTest.php:24

3) Tests\Feature\Pet\StoreTest::storeSuccess
Keyword validation failed: Required property 'tag' must be present in the object

/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:76
/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:98
/path/to/laravel-petstore-api/tests/Feature/Pet/StoreTest.php:23

FAILURES!
Tests: 8, Assertions: 13, Failures: 3.

おわりに

API仕様に準拠しているか、実装が変わるたびに検証されていないと、しだいにAPI仕様が陳腐化し、結果、「今動いている実装が正しい」ということになりがちです。
もちろん、動いているAPIから仕様書を自動生成するというアプローチもありますが、以下のような問題があると思います。

  • API仕様を変更するのにPHPを触らなければいけない
  • API仕様をPHPのアノテーションで定義する形式だと、検査されずに陳腐化する可能性がある

なので、先にAPI仕様をOpenAPI v3形式で作成してから、クライアントチームと合意をとりながら開発を進めていくのが良いのかなと個人的には考えています。

この記事が、日々、多くのAPIを実装、保守しているサーバーサイドエンジニアの助けになれば幸いです。
ではでは。

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

GitHub Actions で PHP の CI/CD をする

EC-CUBE の CI/CD を GitHub Actions で実装してみたので、メモ程度に知見をまとめてみます。

該当の Pull Request はこちら
- 4系 https://github.com/EC-CUBE/ec-cube/pull/4337
- 2系 https://github.com/EC-CUBE/eccube-2_13/pull/312

GitHub Actionsは 2019年10月2日現在、限定パブリックベータですので、正式リリース時には変更される場合があります

実現したいこと

  • 以下の環境をマトリクスで実行したい
    • Linux, Windows
    • PHP5.4〜7.3
    • PostgreSQL/MySQL/SQLite3
  • PHPUnit によるユニットテスト
  • Codeception による E2Eテスト

EC-CUBE は、Travis CI 及び AppVeyor で CI を実行しており、これを GitHub Actions へ移植することにします

直面した課題

GitHub Actions にインストールされている PHP バージョン

  • ubuntu-18.04 は PHP7.1〜7.3
  • ubuntu-16.04 は PHP5.6〜7.3
  • windows-2019, windows-2016 は PHP7.3

上記のような状況ですので、 PHP5.4, 5.5 の環境は自力で構築する必要があります。
GitHub Actions は、独自の Actions を作成できますので、 PHP5.4〜7.3 を Linux, Windows でマトリクス実行する Actions を作成しました。

Setup PHP environment

以下のような感じで実行できます

jobs:
  build:
    runs-on: ${{ matrix.operating-system }}
    strategy:
      matrix:
        operating-system: [ ubuntu-18.04, windows-2019 ]
        php: [ '5.4', '5.5', '5.6', '7.1', '7.2', '7.3', '7.3.3' ]
    name: PHP ${{ matrix.php }} sample
    steps:
      - uses: actions/checkout@master
      - name: Setup PHP
        uses: nanasess/setup-php@master
        with:
          php-version: ${{ matrix.php }}
      - run: php my_script.php
  • Linux で apt の利用可能なバージョンは apt を使用
    • apt が利用できない場合は phpenv でビルド
  • Windows 環境は chocolatey を利用
  • ubuntu-16.04 は標準パッケージと小々異なる構成(OpenSSL1.1 がインストールされている)のため、 OpenSSL1.0 と libpq.so をソースから構築している

独自の Actions を作りたい場合は、 JavaScript または Docker コンテナを使用する必要があります。 actions/typescript-action テンプレートや actions/javascript-action, actions/hello-world-docker-action が利用できます。

Windows 環境は Composer がインストールされていない

こちらも独自の Actions を作成しました

Composer installer of Github Actions

ChromeDriver の実行

Codeception を実行するために ChromeDriver を設定する必要があります。
以下のような step を作成しました

    - name: Setup chromedriver
      run: |
        sudo apt-fast install -y xvfb debconf-utils screen google-chrome-stable
        wget -c -nc --retry-connrefused --tries=0 http://chromedriver.storage.googleapis.com/2.43/chromedriver_linux64.zip
        unzip -o -q chromedriver_linux64.zip
        export DISPLAY=:99
        ./chromedriver --url-base=/wd/hub &
        echo ">>> Started chrome-driver"
        sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &

ここで注意する必要があるのが、環境変数 DISPLAY の設定です。
jobs.<job_id>.steps.env で設定しても、 ChromeDriver が動きません。
export で設定する必要があります。

ちなみに、ファイルダウンロードのテストをする必要があるため、ヘッドレスモードは使用しません。

2019年10月3日追記 Actions を作成しました
nanasess/setup-chromedriver

こんな感じで記述できます

    - name: setup-chromedriver
      uses: nanasess/setup-chromedriver@master
      with:
        chromedriver-version: '77.0.3865.40'

    - name: Run chromedriver
      run: |
        export DISPLAY=:99
        chromedriver --url-base=/wd/hub &
        echo ">>> Started chrome-driver"
        sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
        echo ">>> Started xvfb"

Windows 環境での MySQL のテスト

Linux 環境では MySQL Server が動いているのですが、 Windows 環境ではクライアントしかインストールされてないため、自前でインストールします

    - name: Setup to database
      run: |
        choco install -y mysql --version 5.7.18
        mysql --user=root -e "CREATE DATABASE `myapp_test`;"
        mysql --user=root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';FLUSH PRIVILEGES;"
        mysql --user=root --password=password -h 127.0.0.1 -e "SELECT version();"

PostgreSQL のテスト

Linux では jobs.<job_id>.services で PostgreSQL を実行できます

    services:
      postgres:
        image: postgres:11
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: password
          POSTGRES_DB: postgres
        ports:
          - 5432:5432
        # needed because the postgres container does not provide a healthcheck
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

参考 https://github.com/actions/example-services/blob/master/.github/workflows/postgres-service.yml

MailCatcher を使用したテスト

Linux では jobs.<job_id>.services で Docker コンテナを実行します

    services:
      mailcatcher:
        image: schickling/mailcatcher
        ports:
          - 1080:1080
          - 1025:1025

Windows は gem でインストールします。
shell: bash を指定するのがポイント。デフォルトの cmd ではバックグラウンドで実行できません。。。

    - name: Setup MailCatcher
      run: gem install -N mailcatcher -v 0.6.5
      shell: bash
    - name: Run to MailCatcher
      run: mailcatcher &
      shell: bash

マイクロサービスの実行

EC-CUBE の E2E テストでは、 EC-CUBEオーナーズストア をエミュレートした Docker コンテナを実行し、プラグインインストールなどのテストをしています。
これを jobs.<job_id>.services で動かしたかったのですが、、 Volume マウントするディレクトリが services を初期化する段階では生成できないため、 docker run コマンドで実行するしかないようです

    ## $ {PWD}/repos does not exist so service cannot be started
    - name: Run package-api
      run: docker run -d --rm -v ${PWD}/repos:/repos -e MOCK_REPO_DIR=/repos -p 8080:8080 eccube/mock-package-api

本当は以下のように書きたい

    services:
      package-api:
        image: eccube/mock-package-api
        env:
          MOCK_REPO_DIR: '/repos'
        ports:
          - 8080:8080
        volumes:
          - $GITHUB_WORKSPACE/repos:/repos

デプロイする

GitHub Actions には AWS CLI や Azure CLI コマンドもインストールされているため、各種クラウドへのデプロイも柔軟にできます。

ここでは、 GitHub にて release の発行をすると、自動的に ZIP 及び tar.gz のパッケージを生成し、 Assets にアップロードするようにしました。

スクリーンショット 2019-09-28 0.59.27.png

コードは長いので こちら を参照してください。

ポイントは、以下のようにすることで、タグ名をパッケージのファイル名にできます

    - name: Packaging
      working-directory: ../
      env:
        TAG_NAME: ${{ github.event.release.tag_name }}
        REPOSITORY_NAME: ${{ github.event.repository.name }}
      run: |
        tar czfp eccube-$TAG_NAME.tar.gz $REPOSITORY_NAME
        zip -ry eccube-$TAG_NAME.zip $REPOSITORY_NAME 1> /dev/null

github.event オブジェクトには、多くの情報が格納されています。
以下のようにして参照できます

    - name: Dump GitHub context
      env:
        GITHUB_CONTEXT: ${{ toJson(github) }}
      run: echo "$GITHUB_CONTEXT"
    - name: Dump job context
      env:
        JOB_CONTEXT: ${{ toJson(job) }}
      run: echo "$JOB_CONTEXT"
    - name: Dump steps context
      env:
        STEPS_CONTEXT: ${{ toJson(steps) }}
      run: echo "$STEPS_CONTEXT"
    - name: Dump runner context
      env:
        RUNNER_CONTEXT: ${{ toJson(runner) }}
      run: echo "$RUNNER_CONTEXT"
    - name: Dump strategy context
      env:
        STRATEGY_CONTEXT: ${{ toJson(strategy) }}
      run: echo "$STRATEGY_CONTEXT"
    - name: Dump matrix context
      env:
        MATRIX_CONTEXT: ${{ toJson(matrix) }}
      run: echo "$MATRIX_CONTEXT"

まとめ

まだパブリックベータ版ということもあり、 PHP で CI/CD をする環境が充実しているとは言い難い感じですが、いろいろ頑張れば柔軟に対応できそうです。

またナレッジがたまったら追記します

参考

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

PHPで型宣言を使ってみた

WordPressもサポートPHPバージョンが7以上になったので、ちょっと触ってみようかなと思っての覚書。

普通の書き方

<?php
function get_greeding_message($number) {
  return $number;
}
print_r( get_greeding_message( 1 ) );

messageなのに数字返ってきますね。これを型宣言でつぶしてみます。

型を宣言する

<?php
function get_greeding_message( int $number ): string {
  return $number;
}
print_r( get_greeding_message( 1 ) );

普通に書くとこんな感じですね。
ただ、これをこのまま実行しても、エラーにはなりません。

強い型付け

declare(strict_types=1);を書くことで、厳密にチェックすることができます。
https://www.php.net/manual/ja/functions.arguments.php#functions.arguments.type-declaration.strict

<?php
declare(strict_types=1);
function get_greeding_message( int $number ): string {
  return $number;
}
print_r( get_greeding_message( 1 ) );

ここまで書くと、実行時にエラーがでます。

PHP Fatal error:  Uncaught TypeError: Return value of get_greeding_message() must be of the type string, integer returned in /home/ec2-user/test.php:4
Stack trace:
#0 /home/ec2-user/test.php(6): get_greeding_message(1)
#1 {main}
  thrown in /home/ec2-user/test.php on line 4

動くようにする

<?php
declare(strict_types=1);
function get_greeding_message(int $number): string {
  return 'Hello No.' . $number;
}
print_r( get_greeding_message( 1 ) );
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【簡単API作成】SwiftSheetを用いてspreed sheetデータを簡単にAPIに変換しよう(PHP版)

1,はじめに:SwiftSheetって何?

皆さんこんにちは。
ふくしま はやと(@MasuraoProg)と申します。

普段は営業として仕事をしている、非エンジニアのおじちゃんです。
今はサンデーエンジニアを自称して、興味あるものを作って挑戦しています。


さて、この度paizaさんのブログにて
SwiftSheetというGooleSpreadSheetをAPIに変更できるできるシステムが
紹介されていました。

めちゃくちゃいいじゃん!


プロダクトを行う際に、まずは簡単に作ってみたいと言うときがあります。
そんなときに、このSwiftSheetはその願いを叶えてくれるサービスになりえます
まさに、MVP向けです。
また、初学者や非エンジニアにとってもめちゃくちゃとっつきやすくていい感じです。
ということで、利用方法等をまとめました。


2,SwiftSheet利用のための準備


2-1,先にGoogleSpreadSheetに情報を記載

練習用に以下のように、GoogleSpreadSheetに記載しました。
1行目に、項目名を記載して、それぞれ適当に記載しています。
GoogleSpreadSheetに記載した後、CSVにて保存してください。


イメージ
000.png


2-2,CSVをSwiftSheetにアップロード

SwiftSheetのWEBページから、アップロードしてください。
なおその際に、
- パスワード
- 有効期限
などを設定することも可能です。


イメージ
111.png


2-3,REST APIが発行される。

発行されたAPIを使用してください。
イメージ
222.png


3-1, 自由に使用してください。

index.php
<?php
header('Content-Type: text/html; charset=UTF-8');

// WebAPIのURL
$url = "https://swiftsheet.app/api/sheet/(あなたのAPI URL)";
// URLの内容を取得し、json形式からstdClass形式に変換し取得
$data = json_decode(file_get_contents($url));

echo('<pre>');
var_dump($data);
echo('</pre>');


object(stdClass)#6 (1) {
["data"]=>
array(5) {
[0]=>
object(stdClass)#1 (4) {
["会社名"]=>
string(15) "AAA株式会社"
["業種"]=>
string(6) "商社"
["住所"]=>
string(21) "東京都港区AAA町"
["電話"]=>
string(12) "03-aaaa-aaaa"
}
[1]=>
object(stdClass)#2 (4) {
["会社名"]=>
string(15) "BBB株式会社"
["業種"]=>
string(12) "人材紹介"
["住所"]=>
string(17) "東京都港BB町"
["電話"]=>
string(7) "03-BBBB"
}
(以下略)


以上です。
簡単なサービス/テストにてAPIが必要な場合は、
パスワード等を設定して、ぜひご利用してみてください!

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

PHPで複数検索を書くときにbit列に分割して実装した.

はじめに

最近宮蘭フェリーハッカソンに向けてSNSもどきを作ってるんですが,ジャンルで分類されているDBにフロント側から複数のジャンルで検索をかけたいときにサーバー側はどういう数字を受け取ればいいんだろうということを考えていてbitを使ってみることにしました.

bit列

今回だと8種類ジャンルがあったので渡された数字が4(=00000100)だと3つめのジャンルで検索かけるというような実装をするようにしました.

実装例

$genre_num = 4;//検索をかけたいジャンル
$genre_bit = decbin($genre_num);
$zero = "";
for($i=0;$i<8-strlen($genre_bit);$i++){
  $zero = $zero."0";
}
$genre_bit = $zero.$genre_bit;
$sql = "SELECT * FROM table";
$flag = false;
for($i=7;$i>=0;$i--){
if($genre_bit[$i] == "1"){
  if($flag == false){
    $sql = $sql." WHERE genre=".(7-$i+1)." ";
    $flag = true;
  }
  else{
    $sql = $sql."OR genre=".(7-$i+1)." ";
  }
}

\$ genre_numに検索するための数字が入っているのでdecbin()に渡して二進数の文字列にしています.
次に今回は8種類なので最初の方を0埋めします.そして文字列の後ろから比較していき,1になっている部分でSQL文を追加していきます.そして最後にSQLを実行すれば多分動きます.
$flagは最初のWHERE句を呼ぶために立てているので一度1を見つけたらflagが立ってそこには入らなくなります.

終わりに

正直もっといい方法あるんじゃないかってすごく思うんですけど今回自分なりに工夫して実装してみました.意見とかマサカリとかお待ちしております.

補足

このプログラムは検索部分のみを切り出してきたものなのでそもそも\$ genre_numにはおかしな数字が入っていないとします(関数を呼ぶ時点で排除しているので).

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

フィリピンIT Coding Boot Camp 6ヶ月コースの4ヶ月目で思うこと

現在、フィリピンのセブでもないマニラでもないどこかの涼しい雨ばかりの街でIT Boot Camp(6ヶ月)に参加した僕が感じていることやIT勉強したい、身に付けたいけど最善の方法って結局学校?それとも働いた方がいい?などの答えのヒントに少しでもなればなぁと思います。(あくまで全て一個人の僕の感想です)

そもそもIT Boot Campて何?

これは、知らない方もいると思いますが、このIT Boot Campはビリーズブートキャンプの仲間ではなく、アメリカやイギリスなどでも開催されているITの知識を詰め込む集中講座のようなものです!

IT Boot Campの特徴
-超短期集中
-勉強する範囲が6ヶ月コースなどになってくると結構広い。
-授業は全て英語
-毎日コード書く生活漬け
-(他は知りませんが)インターンシップ先を用意してくれる
...こんなところですかね。
とりあえずまとめると、6ヶ月と言う期間でコードを書きWebsiteを作れるように学習し、そしてクライアントを獲得できるまでに成長させよう!
みたいなノリのコースですね。

実際、仕事を自分で得れるほどまでに成長するの?

ここが全員気なっているところですよね〜。
結論から言います。(これもただの僕の意見です
ただ言われてるだけのことをこなして学校が終わり次第家に帰っていたら絶対無理です。
ただ誤解して欲しくないのは、別にコースを否定している訳ではなく、ただお金をいただいてオンライン上でクライアント様の希望を目に見える形にすると言うのはとても大変と言うことです。6ヶ月のコースでそれが完璧に身につくはずはありません。いくら学校に高い授業料を払っていても、自分で主体的に動かなければ生き残ってはいけません。

それならいかない方がいい?

そう思いがちですが、ちょっと待ってください。
だからこそIT Boot Campでは、エンジニアとして一番大事なスキルである自分をアップデートする方法を学べるんです。
これは本当に日常生活でも役に立つんですが、まず知らない事をすぐに調べると言う情報の収集の癖がつきますし、
その情報収集の仕方やcオーディングでバグが出た際の解き方など応用がきくテクニックもひたすら教えてくれます。

でも授業では、実際そのやり方を教えてくれるだけで自分のスキルとして落とし込むまでの時間は取ってくれないんですよ。
なので、終わった〜って家に帰ってしますと綺麗さっぱり忘れてまたぐるぐる同じことを学んで時間だけ過ぎて言ってしまいます。
要は、自分で学んだことをしっかりと整理しアウトプットできればそれなりに成果の上がるコースだと言うことです。

自分なりのメリットとデメリット

メリット
・コーディングの基礎は身につく(HTML、CSS、JavaScriptなど)。
・マネタイズやクライアントの獲得の仕方まで一応体系的に教えてくれる。
・やる気がある人にとってはいいスピード感でどんどん新しい分野や言語を教えてくれる
・仕事上だと何回もきくことに抵抗があるが生徒の立場なら何回も納得がいくまで聞ける。
・授業はほぼどこでも英語なので英語が上達する
・エンジニアになりたいけど、何をどうやって始めたらいいの?って言う方には唯一お勧め!
デメリット
・アカデミックな内容も多いので実践で活かせないこともある。
前回の記事でも書きましたが、やっぱり実際に企業に入ろうとすると現実的にたかだか数ヶ月しかコーディングかじってないと言う現実を痛感するので自分でアンテナをしっかりはってどの言語やどんなスキルが生きるかを模索し続けながら勉強していく必要がある事。
(これに関してはいいきっかけにもなるので、メリットでもありますね!)
・日本の案件は全くカリキュラム内では取ろうとしないので海外案件を狙うというハードルの高さが少しある。
・カルチャーギャップで苦しむ時がある(来てみて感じてみてください笑)

最後に

未経験で企業に入っていきなりコーディングを詰め込まれても正直限界がありますが、そのあとの仕事は自分で取らなくても用意されていると言うことになります。
一方、こういったコースを受講しいいご縁にも恵まれ、フリーランスとして活躍できれば自分の仕事を選ベルし嫌なら断れます。
値段だって交渉できます。とにかく規制がありません。自由です。

これは勧誘でもないし、IT Coding Boot Campというシステムへの悪口でもありません。
ただ、実際に足を踏み入れたことがない方や、少しでも興味がある方達がこの記事をみて『やっぱやーめた』とかなったり『背中を押せたり』できればなと思い本音で全て書いてあリます。
なので、なんか言っていることが矛盾していればこのコースのいい側面もあるけど嫌なところや不満も抱えながらコーディングに励んでいたということです笑

以上、僕の体験談でした。
何かありましたらコメントください。
読んでいただきありがとうございました。

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

基礎的エラーだった [ Warning ]: array_merge(): Argument #2 is not an array COREPATH/classes/asset.php @ line 116

Warning!

Fuel\Core\PhpErrorException [ Warning ]:
array_merge(): Argument #2 is not an array
COREPATH/classes/asset.php @ line 116

111 {
112 \Errorhandler::notice('Asset with this name exists already, cannot be overwritten.');
113 return $exists;
114 }
115
116 static::$instances[$name] = new \Asset_Instance(array_merge(static::$default_config, \Config::get('asset'), $config));
117
118 if ($name == 'default')
119 {
120 static::$
instance = static::$_instances[$name];
121 }


このエラーは「第二引数が配列で返ってきていないよ」という文。

原因

URLで指定したアクション先にコントローラーの記述を書いていなかったため。
(誤って本来viewディレクトリ内で書くべきのHTMLをアクション先のディレクトリ内に記述していたという凡ミス)

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

新人エンジニアが企業面接で自分の市場価値を聞かれ「あ、0円だな」と思った話と改善策。

目的

この記事は自分がプログラミング学習を始める前にもう少しこういった心持ちや戦略をしっかり立てて始めてればよかったのかなということを共有したものです。

この話の背景説明

昨今騒がれているフィリピンのIT留学を現在実際に在籍中の僕が6ヶ月コースの4ヶ月目にしてよしこれからは座学じゃなくて実践だ!!と思い立ち初めて面接を受けた時のお話です。

お金を 「払って」 ITを身につけるかお金を 「もらって」 ITを身につけるか

僕が受けた企業の事業内容は本当に素晴らしく心躍るものでした。
気に入りすぎてそこの一社しかまず受けたくない思いもう他の企業には目もくれずそことの面接を心待ちにしてました。(これがもう戦略的には間違い笑)
代表の方も非常に気さくな方で話しているうちに本当にいい会社だなと感じ、いざ自分を売り込もうとした時に代表の方から「うん、あなたが作ったWebsiteは実践のプレッシャーの中で作れば1ヶ月ないくらいで完成できるものだね。」と。
もうぐーのねも出ないとはこのことでした。(これでも結構コーディングの勉強はしている方でした)
実際に、いろいろなことを学んでも実際にお金を生み出せるスキルはその企業が必要としているスキルが当然中心となります。
そんな感じで面接も進み、最後に「お給料はいくらがいいですか?」と、今までお給料は会社が決めた額をもらって文句をブーブー言いながら働いてた割にいざこの質問をぶつけられた時の答えは。。。わ、わかんねぇ(汗でした。
もちろん100万円欲しいですって言ったら即スカイプ面談を切られると思いますので笑
自分を数字に表すってとっても大変だなと痛感しました。。

2つの問題点と改善策

こういう状況になったのは主に2つの思考が欠けていたから
1.実際の企業の案件や、仕事内容に対する「実践」への意識の薄さ。
2.自分のやってきたことの「整理」が全くできていなかった。

1.結論から言うとこれはバランスを失ってしまったと言うことに尽きます。
もちろんITコースに参加した当初はもちろんあったのですが、どんどんコースが進んでいくにつれてコーディングの勉強に追われる日々になり
自分は当分実践なんておこがましいと思ってしまい、求人や調べるのをやめてしまったんですね。。。
これが本当によくなかった。

2.自分がやってきたことの整理はとにかくアウトプットすることを怠ったことの跳ね返りでした。。
GitHub や こちらのQiitaなどいくらでもシェアする方法はあったのですが怠りました。
なので、この記事もそうですが、少しでも自分の知識などを外に出し、同じ境遇の方や誰かの参考になればと考えています。

ちなみに、こちらが企業に見せた4ヶ月目の自分のWebsiteです。→http://sodaihirano.com/

まとめ

まぁ、結局思ったことは、
・IT留学してても結局は自分でアンテナはって案件や企業調べてニーズを図りつつ、授業で学んだ知識でどうやって企業に属した時にいかせるかを常に考え、そこでの気づきなどをアウトプットしていかないと一生懸命机に向かって何も実践を想定せずコード書いてても実質的な自分の価値は上がらない。

・逆に少しずつでもアウトプットして残せるものがあればそれを見せてこれ作れますって言えますよね。だからお給料は100円を希望します。って言える。

それでも、0円よりはマシですよね。笑

以上、僕の体験談でした!
これから少しずつ技術系の記事もあげながら自分の価値に念頭を置き書き進めていきたいと思います!
お読みいただきありがとうございました!

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

Composerいろいろ

バージョンを調べる

$ composer -V
Composer version 1.8.6 2019-06-11 15:03:05

バージョンアップ

composer self-update

Composer管理パッケージのバージョンを調べる

composer sh
composer sho
composer show

上記どれでも良い。
-i は古いと怒られる (You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.) ので注意。

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

株式会社セレスさんの3daysインターンシップに参加した話

株式会社セレス3daysインターンシップ

IMG_8170.JPG

【インターンシップの概要】

Day1:アイディアソン
テーマに沿って新しいメディアを自分達で考えてみましょう! とはいえ、アイディア出しって何からするの?という方もいるかもしれませんが、心配いりません。 しっかりとアイディア出しのノウハウや企画のコツをお教えします!

Day2:ハッカソン
1日目のアイディアソンで出たアイディアを、実際にカタチにしてみましょう! チームで開発を行うので、自分の知らない技術や得意な技術を教え合う事もあります。

Day3:ハッカソン&成果発表
最終日は成果発表を行ってもらいます。開発と同時進行で自分たちの成果をまとめたプレゼンの準備も進めていきましょう。 発表後はプロのエンジニアからフィードバックを行うので、今後の開発に役立ててください!

※サポーターズ引用

1日目は色々なアイデア出しの手法を教えてもらった。

Break the bias

否定、逆説からアイデア出しをする手法。
調べすぎないのも重要

シナリオグラフ

いつ、誰が、どこで、何した、という4点から選択肢をピックアップして物語にすることでアイデアを生み出すフレームワーク

ブレスト

  1. 批判しない
  2. 自由発言
  3. 質より量
  4. アイデアのシナジー 

などなど...

【実際に製作したサービス】

ライブ後の感動をファン同士で共有できるサービス「Yo-in」

好きなアーティストのライブに参戦した後って、なんだか悲しい気分になりませんか?虚無感というか...(特にぼっち参戦だったら)そんな人たちのためにライブ後にファン同士で交流できるサイトを作りました!
ライブ映像というのは全てが映像化されて永遠に保存できるものではなく、「あのとき〇〇が手振ってくれた!」などのその時のその場でしか味わえない感動があると思います。そういった「鮮度の高い感動をその場で共有できるお友達ができたら嬉しい」という方のためのアプリです。自分の参戦するライブが決まったらこのサイトを通じてファンの交流会に参加して仲間を見つけてみませんか?ぜひ「余韻」に浸ってみってください!

【エンジニアの方々のフィードバック】

・今はツイッターで行われている気がする。そっからデータ取ってきたりすればいいかも?
・「発狂」にフォーカス当ててるところ評価する
・最近だとマップアプリで検索するとすぐ出てきそう…
・どういう風な提案かと思いきや二次会提案サービスでちょっと残念。
・ライブ本体にどうやって寄生するのかが難しそうに感じました
・ライブに限らなくてもいいんじゃないじゃなあ。
・いいね!
・作り込まれていてよかったです。
・ライブの後はカラオケや食事に行きたくラリますよね!駅も混んでますし…。
・お疲れ様です。プレゼンがすごく上手でした!ライブの後に飲み会行きたいのは分かります!ただLIVEならではの工夫が欲しいです。今回のデモストーリーだと、普通の飲み屋予約サービスとの違いがわかりませんでした。
・プレゼン資料が見やすく、説明も聞きやすくて良かった
・twiplaなんかで募集してたりするので、専門のサービスを作るのもアリだなと思いました。
・感動を共有したい、帰るまで→ライブ後の居酒屋探し・・・?
・3日間お疲れ様でした!サービスもデザイン含め凄くよくまとまっていました!ライブとかの後に余韻を共有したいというのは非常にニーズがありそうだと思いました。
・発想も面白いと感じ、ライブ会場やレストランとの場所の連携もとれていて機能性も良いと思った。空席が分かるのも、ユーザー・レストランニーズ満たせて良いと感じました。
・マネタイズが少し弱い気がしました。サイトの作りはきれいな感じにまとまっていたと思います。
・デザインがきれいで良かったです。
・余韻で乗っかる系のサービスは良さそうですね。集客は何もしなくてもできそう。
・ライブに限らず色んな余韻を共有しあえるといいのかなとも思ったり

【インターン終えての感想と反省】

・Twig,slim,PHP, Bootstrapなど知らない技術ばかりだったけれどフレームワークやチートシーををとても丁寧に用意してくれていたのでそれなりに形にして実装することができた。
・チームの中でタスクと進捗を共有する時間を十分に取ったことで、時間内に作り上げることができた。
・枝葉末節にとらわれず、とりあえず「最低限動くものを作る」という感覚を養うことができた。
・既存のソーシャルアプリ(twitterなど)との違い、「なぜこのアプリでなければいけないのか?」をうまく伝えられなかった。
・他にも実装したい機能はたくさんあった。

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

phalcon 3.4 centos7 install

phalcon パッケージのリポジトリ情報登録

phalconの最新パッケージは、ここにあります。
https://packagecloud.io/phalcon/stable

ここから
まずは、yumリポジトリを登録します。

[RPM]ですね。
image.png

[RPM]をクリックするとcurlコマンドが出てきますのでコピーしてLinux上で実行します。
image.png

リポジトリのインストールが終わったらphalconがインストールできるか検索してみます。

yum search phalcon

php55u-phalcon-debuginfo.x86_64 : Debug information for package php55u-phalcon
php56u-phalcon-debuginfo.x86_64 : Debug information for package php56u-phalcon
php70u-phalcon-debuginfo.x86_64 : Debug information for package php70u-phalcon
php71u-phalcon-debuginfo.x86_64 : Debug information for package php71u-phalcon
php72u-phalcon-debuginfo.x86_64 : Debug information for package php72u-phalcon
php73u-phalcon-debuginfo.x86_64 : Debug information for package php73u-phalcon
php55u-phalcon.x86_64 : High performance PHP framework
php56u-phalcon.x86_64 : High performance PHP framework
php70u-phalcon.x86_64 : High performance PHP framework
php71u-phalcon.x86_64 : High performance PHP framework
php72u-phalcon.x86_64 : High performance PHP framework
php73u-phalcon.x86_64 : High performance PHP framework

phalconのインストール

php7.2の場合は、
php72u-phalcon.x86_64

php7.3の場合は、
php73u-phalcon.x86_64

今回は、php7.2の環境にいれるので、
php72u-phalcon.x86_64

yum install php72u-phalcon.x86_64

インストールは2箇所にあります。
/usr/lib64/php/modules/phalcon.so
/usr/lib64/php-zts/modules/phalcon.so (スレッドセーフ版)

php --info | grep phalcon

/etc/php.d/50-phalcon.ini
phalcon
phalcon => enabled

phalcon => enabledと表示されていれば、OKです。

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

【PHP】DateTimeクラスのインスタンス呼び出しから、日付文字列出力までワンラインで書く方法

毎回毎回忘れてしまうので個人的なメモです。

PHPで日付時刻の文字列を出すときは、dateでやってしまえば手っ取り早いのは十分承知です。

今回は、DateTimeクラスで、1行1発で出力させる方法です。

echo (new DateTime())->format('Y-m-d H:i:s');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPの参照渡しと値渡しの説明

PHP参照渡しまとめ

RAMとアドレス

変数はコンピュータの中のメモリRAMに値が保存される。メモリにはアドレスが有ってどのアドレスどんな値が入っているかを管理している。

値渡し

メモリーの違うアドレスに値をコピーして保存する方法。元の変数と別のアドレスで管理される。

参照渡し

メモリーの同じアドレスを参照する方法。メモリーの同じアドレスを使うので元変数でも渡した後の変数のどちらかでも変更すると同じ値になる。

PHPでは

関数の引数は値渡しになる。
ただしオブジェクトは参照渡しなる。
引数、変数の先頭に&を付けると参照渡しになる。

<?php
//------------------------------------
$name1 = 'Hikakin';
$name2 = &$name1; // ← name2はname1と同じアドレスの変数を参照する
$name3 = $name1; // ← name3はname1と違うアドレスに変数を保存する
$name1 = 'Seikin';  // name1 を Seikinにする
echo $name2."\n"; // Seikin  参照渡しでname1と同じアドレスを参照しているのでSeikinに変わっている
echo $name3."\n"; // Hikakin 値渡しでコピーした値を持っているのでHikakinのまま

//------------------------------------
$val1 = 'abc';
function funcOne($val) // ← 値渡し 別アドレスに値をコピー
{
    $val = 'xyz'; 
}

funcOne($val1);
echo $val1 . "\n"; // abc

//------------------------------------
$val2 = 'abc';
function funcTwo(&$val) // ← 参照渡し caller側と同じアドレスの変数を参照
{
    $val = 'xyz';
}

funcTwo($val2);
echo $val2 . "\n"; // xyz

//------------------------------------
$list1 = ['a','b','c'];
function funcThree($val) { // ←Arrayも値渡し。別アドレスにコピーされている
    $val = ['x','y','z'];
}
funcThree($list1);
var_dump($list1); // 'a','b','c'

//------------------------------------
$list2 = ['a','b','c'];
function funcFour(&$val) {
    $val = ['x','y','z'];
}
funcFour($list2);
var_dump($list2); // 'x','y','z'

//------------------------------------
$obj = new stdClass;
$obj->name = 'Hikaru';
function funcFive($obj) { // ←オブジェクトは有無を言わさず参照渡し
    $obj->name = 'Repezen';
}
funcFive($obj);
var_dump($obj); // 'Repezen  ←レペゼンが侵食していく
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでインデントを不要にしてみた【php】

0. 動機

「pythonはc言語やjavaでいうブロックの代わりにインデントを使うんだよ」というのを そういうものだと受け入れたら、今までできたはずの言語で構文エラーしまくることになる事例を知った。
僕自身はpythonについて、ほとんどしらない。
唯一知ってるのは「pythonがインデントに意味を持たせたのは有害」ということだけ。
→pythonには「1行1ステートメント」という考え方があり、だからこそブロックの括弧やセミコロンは不要[1]というのは後から知った。

そこで、インデントの代わりにブロック{ }を使って記述したpythonコードを、正しいpythonコードに置き換えるプログラムを作ろうという気になった。
phpがインストールされているサーバからみんなが使えるように、phpで作る。

1.プログラムの構成

  1. テキストエリアから、インデントの代わりにブロックで書かれたpyファイルの中身を受け取る(元コンテンツと呼ぶことにする)
  2. 元ファイルのブロックはインデントに置き換え、元ファイルのインデントは捨てる
  3. 結果を表示

1-1.

次のようなblock2indent.htmlを作ってみた。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br><input type="submit">
        </form>
    </body>
</html>

余談だが、Eclipseで「PHP 開発ツール (PDT)」をインストールし、エディタに「<」とだけ打ち込むと、入力候補としてという選択肢が表示される。これを無視してバックスペースキーを押して「<」を消すと、cssやhtmlなど様々なコードのテンプレートが選べるようになる。これでhtml 4.01 strictのコードなども容易にかける。

block2indent.htmlをクロームで開くと、図1.1.1のように表示され、「送信」を押すと、「ファイルが見つかりません」のエラーへ遷移した。
1.png

図1.1.1 block2indent.html

1-2.

block2indent.htmlを名前変更しblock2indent.phpとした。
そして、次のように追記した。

<?php
$samplePy=
"
class Man:{
    def __inin__(self, name):{
        self.name = name;
        print(\"インスタンス生成および初期化に成功\");
    }

    def hello(self):{
        print(\"Hello \" + self.name + \"!\")
    }
}

m = Man(\"David\");
m.hello();
";

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();


function printHtml()
{
    print
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br>
            ③<input type="submit">
        </form>
    </body>
</html>';
}

    function killIndent($indentAlive)
    {
        $indentDying = preg_replace("/\A( |\t)+/i", "", $indentAlive);
        return preg_replace("/(\n|\r|(\r\n))( |\t)+/i", "$1", $indentDying);
    }

    function block2indent($blockAlive)
    {
        //行頭以外でブロックが開始していたら、ブロック開始直前に改行挿入
        //同様に、ブロックの終わりも改行を強制
        $blockDying = putNewLineBeforeBlockOpen($blockAlive);
        $blockDying = putNewLineAfterBlockClose($blockDying);

        //各行ごとの「深さ」を調べ、その分だけスペース4つを挿入
        $blockDying = createIndent($blockDying);

        //ブロックをなくす
        return killBlock($blockDying);
    }

        function putNewLineBeforeBlockOpen($blockOpenWhileARow)
        {
            return preg_replace("/(.+){/i", "$1\r\n{", $blockOpenWhileARow);
        }

        function putNewLineAfterBlockClose($blockCloseWhileARow)
        {
            return preg_replace("/}(.+)/i", "}\r\n$1", $blockCloseWhileARow);
        }

        function createIndent($noIndent)
        {
            //改行コードを統一
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            $noIndent = preg_replace("/\r([^\n])/i", $returnCode."$1", $noIndent);
            $noIndent = preg_replace("/([^\r])\n/i", "$1".$returnCode, $noIndent);

            //改行区切りで配列に入れる
            $lines = explode($returnCode, $noIndent);

            //各行に対して、深さを調べる
            $i = 0;
            $depth = 0;
            $lineIdx2depth = array();
            foreach($lines as $line)
            {
                if(strpos($line, "{")===0)
                    $depth++;

                $lineIdx2depth[$i] = $depth;

                if(strpos($line, "}")!==false)
                    $depth--;

                $i++;
            }

            //書き込む
            $i = 0;
            $ownIndent = "";
            foreach($lines as $line)
            {
                for($j=0; $j < $lineIdx2depth[$i]*4; $j++)
                    $ownIndent .= " ";
                $ownIndent .= $line;
                $ownIndent .= $returnCode;
                $i++;
            }

            return $ownIndent;

        }


        function killBlock($blockAlive)
        {
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            return preg_replace("/".$returnCode."( |\t)*{( |\t)*/i", "", preg_replace("/".$returnCode."( |\t)*}( |\t)*".$returnCode."/i", "", $blockAlive));

        }

printHtml関数が実行されると、1-1でみたblock2indent.htmlの内容がphpにより出力される。
しかし今回はif(true)に対するelseとして記述されているため、printHtmlは呼び出されない。その代わり、$samplePyに対してkillIndent関数やblock2indent関数が適用されたものが出力される。

$samplePyの内容は、次の通り、{ }を用いて書かれた「謝ったpythonコード」である。

class Man:{
    def __inin__(self, name):{
        self.name = name;
        print("インスタンス生成および初期化に成功");
    }

    def hello(self):{
        print("Hello " + self.name + "!")
    }
}

m = Man("David");
m.hello();

これにkillIndent関数が適用されると、次のようになる。

class Man:{
def __inin__(self, name):{
self.name = name;
print("インスタンス生成および初期化に成功");
}

def hello(self):{
print("Hello " + self.name + "!")
}
}

m = Man("David");
m.hello();

さらにblock2indent関数が適用されると、次のように正しいpythonコードが出力される。

class Man:
    def __inin__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("David");
m.hello();

実際には次の手順に従って実行されている。

  1. {の直前と}の直後に改行がなければ挿入
  2. 各行ごとに{ }の深さ$depthの取得
  3. 各行ごとに4×$depth個だけ空白を先頭に追加
  4. {}を行ごと削除

1-3

プログラム自体は完成したので、後はサーバにアップロードして、外部からhttp通信でアクセスできるようにすることを考えよう。

1-2で書いたblock2indent.phpに対し、

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

の部分の先頭に/を追加し、

//*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

として、サーバへアップロードするだけでよい。
こうすることで、サーバ上のblock2indent.phpは次のように振舞うことになる。

  1. 自身へ何もpost送信されていない場合(=最初にアクセスされたとき)
    1. 1-1で書いたblock2indent.htmlと同じものを出力
    2. 送信ボタンが押されると、 自分自身へテキストエリア内の文字列をpost送信
  2. 自身へテキストエリア内の文字列がpost送信された場合
    1. 1-2のアルゴリズム通り、pythonコードへ変換を行う
    2. 結果を出力する

2.png
図1.3.1 「誤ったpythonコード」を入力する例

実際、図1.3.1のように入力し、送信ボタンを押すと、次のように正しいコードが出力された。(但し、改行コードの選択肢に
は含めていないので、「ソースを表示」から見る必要がある。)

class Woman:
    def __init__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("Alice");
m.hello();

入力例のインデントを多少変えても、出力は同じになる。

2. このプログラムを利用したい方

つまらないものですが、http://rights-for.men/block2indent.php からどうぞ。

3 n.参考

[1]https://python.keicode.com/lang/control-basic-rule.php

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

pythonでも中括弧{ }でブロックを作れるようにしてみた【php】

0. 動機

「pythonのブロックはc言語やjavaみたいに中括弧({ })でやるんじゃなくて、インデントを使うんだよ」というのを そういうものだと受け入れたら、今までできたはずの言語でうっかり同じことをしてしまい、構文エラーしまくることになる事例を知った。
僕自身はpythonについて、ほとんどしらない。
唯一知ってるのは「pythonがインデントに意味を持たせたのは有害」ということだけ。
→pythonには「1行1ステートメント」という考え方があり、だからこそブロックの括弧やセミコロンは不要[1]というのは後から知った。

そこで、インデントの代わりに{ }を使ってブロックを記述したpythonコードを、正しいpythonコードに置き換えるプログラムを作ろうという気になった。
phpがインストールされているサーバからみんなが使えるように、phpで作る。

1.プログラムの構成

  1. テキストエリアから、インデントの代わりにブロックで書かれたpyファイルの中身を受け取る(元コンテンツと呼ぶことにする)
  2. 元ファイルのブロックはインデントに置き換え、元ファイルのインデントは捨てる
  3. 結果を表示

1-1.

次のようなblock2indent.htmlを作ってみた。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br><input type="submit">
        </form>
    </body>
</html>

余談だが、Eclipseで「PHP 開発ツール (PDT)」をインストールし、エディタに「<」とだけ打ち込むと、入力候補としてという選択肢が表示される。これを無視してバックスペースキーを押して「<」を消すと、cssやhtmlなど様々なコードのテンプレートが選べるようになる。これでhtml 4.01 strictのコードなども容易にかける。

block2indent.htmlをクロームで開くと、図1.1.1のように表示され、「送信」を押すと、「ファイルが見つかりません」のエラーへ遷移した。
1.png

図1.1.1 block2indent.html

1-2.

block2indent.htmlを名前変更しblock2indent.phpとした。
そして、次のように追記した。

<?php
$samplePy=
"
class Man:{
    def __inin__(self, name):{
        self.name = name;
        print(\"インスタンス生成および初期化に成功\");
    }

    def hello(self):{
        print(\"Hello \" + self.name + \"!\")
    }
}

m = Man(\"David\");
m.hello();
";

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();


function printHtml()
{
    print
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br>
            ③<input type="submit">
        </form>
    </body>
</html>';
}

    function killIndent($indentAlive)
    {
        $indentDying = preg_replace("/\A( |\t)+/i", "", $indentAlive);
        return preg_replace("/(\n|\r|(\r\n))( |\t)+/i", "$1", $indentDying);
    }

    function block2indent($blockAlive)
    {
        //行頭以外でブロックが開始していたら、ブロック開始直前に改行挿入
        //同様に、ブロックの終わりも改行を強制
        $blockDying = putNewLineBeforeBlockOpen($blockAlive);
        $blockDying = putNewLineAfterBlockClose($blockDying);

        //各行ごとの「深さ」を調べ、その分だけスペース4つを挿入
        $blockDying = createIndent($blockDying);

        //ブロックをなくす
        return killBlock($blockDying);
    }

        function putNewLineBeforeBlockOpen($blockOpenWhileARow)
        {
            return preg_replace("/(.+){/i", "$1\r\n{", $blockOpenWhileARow);
        }

        function putNewLineAfterBlockClose($blockCloseWhileARow)
        {
            return preg_replace("/}(.+)/i", "}\r\n$1", $blockCloseWhileARow);
        }

        function createIndent($noIndent)
        {
            //改行コードを統一
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            $noIndent = preg_replace("/\r([^\n])/i", $returnCode."$1", $noIndent);
            $noIndent = preg_replace("/([^\r])\n/i", "$1".$returnCode, $noIndent);

            //改行区切りで配列に入れる
            $lines = explode($returnCode, $noIndent);

            //各行に対して、深さを調べる
            $i = 0;
            $depth = 0;
            $lineIdx2depth = array();
            foreach($lines as $line)
            {
                if(strpos($line, "{")===0)
                    $depth++;

                $lineIdx2depth[$i] = $depth;

                if(strpos($line, "}")!==false)
                    $depth--;

                $i++;
            }

            //書き込む
            $i = 0;
            $ownIndent = "";
            foreach($lines as $line)
            {
                for($j=0; $j < $lineIdx2depth[$i]*4; $j++)
                    $ownIndent .= " ";
                $ownIndent .= $line;
                $ownIndent .= $returnCode;
                $i++;
            }

            return $ownIndent;

        }


        function killBlock($blockAlive)
        {
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            return preg_replace("/".$returnCode."( |\t)*{( |\t)*/i", "", preg_replace("/".$returnCode."( |\t)*}( |\t)*".$returnCode."/i", "", $blockAlive));

        }

printHtml関数が実行されると、1-1でみたblock2indent.htmlの内容がphpにより出力される。
しかし今回はif(true)に対するelseとして記述されているため、printHtmlは呼び出されない。その代わり、$samplePyに対してkillIndent関数やblock2indent関数が適用されたものが出力される。

$samplePyの内容は、次の通り、{ }を用いて書かれた「謝ったpythonコード」である。

class Man:{
    def __inin__(self, name):{
        self.name = name;
        print("インスタンス生成および初期化に成功");
    }

    def hello(self):{
        print("Hello " + self.name + "!")
    }
}

m = Man("David");
m.hello();

これにkillIndent関数が適用されると、次のようになる。

class Man:{
def __inin__(self, name):{
self.name = name;
print("インスタンス生成および初期化に成功");
}

def hello(self):{
print("Hello " + self.name + "!")
}
}

m = Man("David");
m.hello();

さらにblock2indent関数が適用されると、次のように正しいpythonコードが出力される。

class Man:
    def __inin__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("David");
m.hello();

実際には次の手順に従って実行されている。

  1. {の直前と}の直後に改行がなければ挿入
  2. 各行ごとに{ }の深さ$depthの取得
  3. 各行ごとに4×$depth個だけ空白を先頭に追加
  4. {}を行ごと削除

1-3

プログラム自体は完成したので、後はサーバにアップロードして、外部からhttp通信でアクセスできるようにすることを考えよう。

1-2で書いたblock2indent.phpに対し、

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

の部分の先頭に/を追加し、

//*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

として、サーバへアップロードするだけでよい。
こうすることで、サーバ上のblock2indent.phpは次のように振舞うことになる。

  1. 自身へ何もpost送信されていない場合(=最初にアクセスされたとき)
    1. 1-1で書いたblock2indent.htmlと同じものを出力
    2. 送信ボタンが押されると、 自分自身へテキストエリア内の文字列をpost送信
  2. 自身へテキストエリア内の文字列がpost送信された場合
    1. 1-2のアルゴリズム通り、pythonコードへ変換を行う
    2. 結果を出力する

2.png
図1.3.1 「誤ったpythonコード」を入力する例

実際、図1.3.1のように入力し、送信ボタンを押すと、次のように正しいコードが出力された。(但し、改行コードの選択肢に<br>は含めていないので、「ソースを表示」から見る必要がある。)

class Woman:
    def __init__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("Alice");
m.hello();

入力例のインデントを多少変えても、出力は同じになる。

2. このプログラムを利用したい方

つまらないものですが、http://rights-for.men/block2indent.php からどうぞ。

3.参考

[1]https://python.keicode.com/lang/control-basic-rule.php

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

【初心者でも簡単!!】WordPressをダウンロード及びインストールする方法

1.事前知識

事前知識として、上記リンクの内容が必要です。

2.WordPressとは

  • WordPress とは、オープンソースのブログソフトウェアである。
  • 言語は PHP で開発されており、データベース管理システムとして MySQL を利用している。
  • 単なるブログではなく コンテンツ管理システム としても利用されている。

3.事前準備

01.png

17.png

MySQLコマンド
# 1.rootユーザーでログイン
$ mysql -uroot

# 2.データベースの作成
mysql> create database wordpress default character set utf8;

# 3.ユーザー作成&権限付与
mysql> GRANT ALL PRIVILEGES ON wordpress.* TO wordpress_user@localhost IDENTIFIED BY 'password';

# 4.権限の反映
mysql> FLUSH PRIVILEGES;

# 5.データベース一覧表示
mysql> show databases;

# 6.mysqlを選択
mysql> USE mysql;

# 7.登録されているユーザを確認
mysql> SELECT user, host FROM user;

# 8.データベース終了
mysql> exit;
  1. Oracle VM VirtulaBox を起動し、 仮想マシン を起動する。
  2. コマンドライン を起動する。
  3. 上記の コマンド を入力する。
  4. 画像のように表示されれば成功です。

4.WordPressのダウンロード

Wordpressのダウンロードコマンド
# 1.フォルダの移動
$ cd /var/www/html

# 2.wordpress-4.4.2-ja.zipをダウンロード
$ wget https://ja.wordpress.org/wordpress-4.4.2-ja.zip

# 3.wordpress-4.4.2-ja.zipの展開
$ unzip wordpress-4.4.2-ja.zip

# 4.wordpress-4.4.2-ja.zipの削除
$ rm wordpress-4.4.2-ja.zip
  1. 上記のコマンドを コマンドライン に入力。

5.WordPressのインストール

URL
例: http://localhost/wordpress/
  1. ブラウザを開き、 https://サーバのホスト名、又はIPアドレス/wordpress/ にアクセスする。(上記のURL参照)
  2. さあ、始めましょう! をクリックする。 02.png
  3. データベース名ユーザー名パスワードデータベースのホスト名テーブル接頭辞 を入力し、 送信 をクリックする。
項目 内容
データベース名 wordpress
ユーザー名 wordpress_user
パスワード password
データベースのホスト名 localhost
テーブル接頭辞 wp_

03.png
4. サイトのタイトルユーザー名パスワードメールアドレス 、 を入力し、 検索エンジンでの表示 にチェックをつけ、 WordPressをインストール をクリックする。
※違う画面に推移した方は 6.構成ファイルのセットアップ
11.png
5. ログイン をクリックする。
12.png
6. ユーザー名パスワード を入力し、 ログイン状態を保存する にチェックをつけ、 ログイン をクリックする。
13.png
7. ダッシュボード が表示されれば成功です。
14.png

6.構成ファイルのセットアップ(インストール出来なかった人)

  1. 赤枠の中身を コピー する。 04.png
  2. アプリケーション から テキストエディター を起動する。 05.png
  3. コピーした内容を 貼り付け し、 保存 をクリックする。 06.png
  4. html ディレクトリをクリックする。 07.png
  5. wordpress ディレクトリをクリックする。 08.png
  6. ファイル名を wp-config.php にし、 保存 をクリックする。 09.png
  7. ブラウザに戻り、 インストール実行 をクリックする。 10.png
  8. 画像のようにWordPressの インストール画面 が表示されれば成功です。 11.png

8.まとめ

WordPressとは

  • WordPress とは、オープンソースのブログソフトウェアである。
  • 言語は PHP で開発されており、データベース管理システムとして MySQL を利用している。
  • 単なるブログではなく コンテンツ管理システム としても利用されている。

事前準備

Apacheのインストールコマンド
# 1.rootユーザーでログイン
$ mysql -uroot

# 2.データベースの作成
mysql> create database wordpress default character set utf8;

# 3.ユーザー作成&権限付与
mysql> GRANT ALL PRIVILEGES ON wordpress.* TO wordpress_user@localhost IDENTIFIED BY 'password';

# 4.権限の反映
mysql> FLUSH PRIVILEGES;

# 5.データベース一覧表示
mysql> show databases;

# 6.mysqlを選択
mysql> USE mysql;

# 7.登録されているユーザを確認
mysql> SELECT user, host FROM user;

# 8.データベース終了
mysql> exit;
  1. Oracle VM VirtulaBox を起動し、 仮想マシン を起動する。
  2. コマンドライン を起動する。
  3. 上記の コマンド を入力する。

WordPressのダウンロード

Wordpressのダウンロードコマンド
# 1.フォルダの移動
$ cd /var/www/html

# 2.wordpress-4.4.2-ja.zipをダウンロード
$ wget https://ja.wordpress.org/wordpress-4.4.2-ja.zip

# 3.wordpress-4.4.2-ja.zipの展開
$ unzip wordpress-4.4.2-ja.zip

# 4.wordpress-4.4.2-ja.zipの削除
$ rm wordpress-4.4.2-ja.zip
  1. 上記のコマンドを コマンドライン に入力。

WordPressのインストール

URL
例: http://localhost/wordpress/
  1. ブラウザを開き、 https://サーバのホスト名、又はIPアドレス/wordpress/ にアクセスする。(上記のURL参照)
  2. さあ、始めましょう! をクリックする。
  3. データベース名ユーザー名パスワードデータベースのホスト名テーブル接頭辞 を入力し、 送信 をクリックする。
項目 内容
データベース名 wordpress
ユーザー名 wordpress_user
パスワード password
データベースのホスト名 localhost
テーブル接頭辞 wp_
  1. サイトのタイトルユーザー名パスワードメールアドレス 、 を入力し、 検索エンジンでの表示 にチェックをつけ、 WordPressをインストール をクリックする。 ※違う画面に推移した方は 6.構成ファイルのセットアップ
  2. ログイン をクリックする。
  3. ユーザー名パスワード を入力し、 ログイン状態を保存する にチェックをつけ、 ログイン をクリックする。
  4. ダッシュボード が表示されれば成功です。

構成ファイルのセットアップ(インストール出来なかった人)

  1. 赤枠の中身を コピー する。
  2. アプリケーション から テキストエディター を起動する。
  3. コピーした内容を 貼り付け し、 保存 をクリックする。
  4. html ディレクトリをクリックする。
  5. wordpress ディレクトリをクリックする。
  6. ファイル名を wp-config.php にし、 保存 をクリックする。
  7. ブラウザに戻り、 インストール実行 をクリックする。
  8. WordPressの インストール画面 が表示されれば成功です。

9.その他

20.png

WordPress起動時に画面が真っ白になってしまった方はこちら↓
WordPressのサイトが真っ白で表示されない場合の対処方法【初心者向け】

10.関連

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