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

CentOS 7にPHP 7.2をインストール(Remi's RPM repository)

はじめに

Remi's RPM repositoryを利用してCentOS7にPHP7.3をインストール
親記事:PHP EOLと各種インストール方法
参考:Remi's RPM repository

サポート

本手法で導入した場合、PHP: Supported Versions/PHP: Unsupported Branchesより、2021-12-06がEOLになると思われる。
それ以降に報告された脆弱性への対応は実施されない可能性がある。

note

  • EPEL repositoryも入れたいたほうが良いかも(本LOGでは入れてない)
# yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
  • インストール後の更新は yum --enablerepo=remi-php73 update

LOG

インストール

# cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)

# yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
... 略

# yum install -y --enablerepo=remi-php73 php which
... 略

各種確認

# which php
/usr/bin/php

# php -v
PHP 7.3.10 (cli) (built: Sep 24 2019 09:20:18) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.10, Copyright (c) 1998-2018 Zend Technologies

# php -i | grep php.ini
Configuration File (php.ini) Path => /etc
Loaded Configuration File => /etc/php.ini

# yum info php
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
 * base: ftp.iij.ad.jp
 * epel: nrt.edge.kernel.org
 * extras: ftp.iij.ad.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp.iij.ad.jp
Installed Packages
Name        : php
Arch        : x86_64
Version     : 7.3.10
Release     : 1.el7.remi
Size        : 10 M
Repo        : installed
From repo   : remi-php73
Summary     : PHP scripting language for creating dynamic web sites
URL         : http://www.php.net/
License     : PHP and Zend and BSD and MIT and ASL 1.0 and NCSA
Description : PHP is an HTML-embedded scripting language. PHP attempts to make it
            : easy for developers to write dynamically generated web pages. PHP also
            : offers built-in database integration for several commercial and
            : non-commercial database management systems, so writing a
            : database-enabled webpage with PHP is fairly simple. The most common
            : use of PHP coding is probably as a replacement for CGI scripts.
            :
            : The php package contains the module (often referred to as mod_php)
            : which adds support for the PHP language to Apache HTTP Server.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CentOS 7にPHP 7.3をインストール(Remi's RPM repository)

はじめに

Remi's RPM repositoryを利用してCentOS7にPHP7.3をインストール
親記事:PHP EOLと各種インストール方法
参考:Remi's RPM repository

サポート

本手法で導入した場合、PHP: Supported Versions/PHP: Unsupported Branchesより、2021-12-06がEOLになると思われる。
それ以降に報告された脆弱性への対応は実施されない可能性がある。

note

  • インストール後の更新は yum --enablerepo=remi-php73 update

LOG

インストール

# cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)

# yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
... 略

# yum install -y --enablerepo=remi-php73 php which
... 略

各種確認

# which php
/usr/bin/php

# php -v
PHP 7.3.10 (cli) (built: Sep 24 2019 09:20:18) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.10, Copyright (c) 1998-2018 Zend Technologies

# php -i | grep php.ini
Configuration File (php.ini) Path => /etc
Loaded Configuration File => /etc/php.ini

# yum info php
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
 * base: ftp.iij.ad.jp
 * epel: nrt.edge.kernel.org
 * extras: ftp.iij.ad.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp.iij.ad.jp
Installed Packages
Name        : php
Arch        : x86_64
Version     : 7.3.10
Release     : 1.el7.remi
Size        : 10 M
Repo        : installed
From repo   : remi-php73
Summary     : PHP scripting language for creating dynamic web sites
URL         : http://www.php.net/
License     : PHP and Zend and BSD and MIT and ASL 1.0 and NCSA
Description : PHP is an HTML-embedded scripting language. PHP attempts to make it
            : easy for developers to write dynamically generated web pages. PHP also
            : offers built-in database integration for several commercial and
            : non-commercial database management systems, so writing a
            : database-enabled webpage with PHP is fairly simple. The most common
            : use of PHP coding is probably as a replacement for CGI scripts.
            :
            : The php package contains the module (often referred to as mod_php)
            : which adds support for the PHP language to Apache HTTP Server.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPテストフレームワーク Codeception の使い方

YiiフレームワークはCodeceptionを公式にサポートしている。本稿ではYiiで使うことを前提にCodeceptionの使い方をざっくりまとめる。Yiiの使い方はこちら

YiiでCodeceptionを利用するための設定

  1. devでアプリケーションを初期化する。(php init で devを選択する)
  2. common/config/test.php 等の構成に従ってテスト用のDBを作成する。

    $ mysql -u root -p
    mysql> DROP DATABASE IF EXISTS yii2advanced_test;
    mysql> CREATE DATABASE yii2advanced_test;
    mysql> DROP USER IF EXISTS 'yii2advanced_test'@'localhost';
    mysql> CREATE USER 'yii2advanced_test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'xxxx';
    mysql> GRANT ALL PRIVILEGES ON `yii2advanced_test`.* TO 'yii2advanced_test'@'localhost';
    
  3. migrationのコードを作成する。
    PHPフレームワーク Yii の使い方#RBAC
    PHPフレームワーク Yii の使い方#DBを定義する
    を参考に。

  4. yii2advanced_testにテーブルを作成する。

    $ ./yii_test migrate --migrationPath=@yii/rbac/migrations
    $ ./yii_test migrate
    
  5. テスト・スイートをビルドする。

    $ vendor/bin/codecept build
    

Codeceptionがサポートするテストの種類と設定ファイル名

各テストの設定ファイルはyii-application/frontend/tests/以下にある。backend, commonも概ね同様。

テストの種類 設定ファイル
単体テスト(Unit Test) unit.suite.yml
機能テスト(Functional Test) functional.suite.yml
受入テスト(Acceptance Test) acceptance.suite.yml

グローバルな設定はyii-application/codeception.ymlに書く。

yii-application/frontend/codeception.yml
namespace: frontend\tests
actor_suffix: Tester
paths:
    tests: tests
    output: tests/_output
    data: tests/_data
    support: tests/_support
settings:
    bootstrap: _bootstrap.php
    colors: true
    memory_limit: 1024M
modules:
    config:
        Yii2:
            configFile: 'config/codeception-local.php'

テストの実行方法

Yiiのテストのクイックスタートガイド参照。

// 全てのテストを実行する
$ vendor/bin/codecept run

// frontend/testsのテストを実行する
$ vendor/bin/codecept run -- -c frontend

// 全ての単体テストを実行する
$ vendor/bin/codecept run unit -- -c frontend

// 指定したテストを実行する
$ vendor/bin/codecept run unit models/ContactFormTest -- -c frontend

テストに使うメソッド

ガイドのdocsのModulesなどを参照。

単体テスト

概ねPHPUnitの様式でテストコードを書いて、上記の要領で実行する。
PHPUnitでいうところのsetUp()が_before()だったり、Codeception独自の様式もある。Codeceptionの便利機能を利用することもできる。詳細はこちら

テストコードの雛形を作成する

// generate:test <suite> <class>
$ vendor/bin/codecept generate:test unit ExampleTest -- -c frontend 

アサーション

verify_that()はempty valueかどうかを検査する。verify_not()はその逆。

verify_that($user->isActivated());
verify_not($user->isBanned());

expectはverifyのエイリアス。verifyよりもTDD/BDDライク。

expect_that($user->isActive());
expect_not($user->isBanned());

expect($test_output)->isInstanceOf('yii\mail\MessageInterface');
expect($test_output)->hasKey('xxxx');
expect($test_output)->equals('xxxx');
expect($test_output)->contains('xxxx');
expect($test_output)->true();
expect($test_output)->false();

expect()はyii-application/vendor/codeception/verify/src/Codeception/function.php
に実装されている。検証に使うメソッドはこちら

例外のテスト

callback中で例外が発生することを検証する方法は下記の通り。

$this->tester->expectException('\yii\base\InvalidArgumentException', function() {
    new ResetPasswordForm('');
});

フィクスチャを使用したテスト

ガイド参照。

yii-application/frontend/tests/unit/models/PasswordResetRequestFormTest.php
use common\fixtures\UserFixture;

public function _before()
{
    $this->tester->haveFixtures([
        'user' => [
            'class' => UserFixture::className(),
            // codecept_data_dir() は yii-application/frontend/tests/_data/
            'dataFile' => codecept_data_dir() . 'user.php'
        ]
    ]);
}

public function testNotSendEmailsToInactiveUser()
{
    // フィクスチャのデータをselectする
    $user = $this->tester->grabFixture('user', 1);

    $model = new PasswordResetRequestForm();
    $model->email = $user['email'];
    expect_not($model->sendEmail());
}

DBを使用したテスト

yii-application/frontend/tests/unit/models/SignupFormTest.php
$user = $this->tester->grabRecord('common\models\User', [
    'username' => 'some_username',
    'email' => 'some_email@example.com',
    'status' => \common\models\User::STATUS_INACTIVE
]);

Codeceptionの本体コード

yii-application/frontend/tests/unit/models/PasswordResetRequestFormTest.php
class PasswordResetRequestFormTest extends \Codeception\Test\Unit // ①
{
    /**
     * @var \frontend\tests\UnitTester // ②
     */
    protected $tester;


    public function _before()
    {
        $this->tester->haveFixtures([  // ③
            'user' => [
                'class' => UserFixture::className(),
                'dataFile' => codecept_data_dir() . 'user.php'
            ]
        ]);
    }

yii-application/vendor/codeception/base/src/Codeception/Test/Unit.php
yii-application/frontend/tests/_support/UnitTester.php
yii-application/frontend/tests/_support/_generated/UnitTesterActions.php

機能テスト

機能テストはブラウザのエミュレーション(HTTP通信を使わず、アプリケーションのインスタンスをコードから直接に実行する)により、ユーザー視点の検証を可能にする。ブラウザからアクセスした場合と異る挙動を示すことがある。

機能テストは受入テストに似ているけれどもWebサーバを必要としないのが大きな違い。受入テストよりも高速に実行でき、詳細なスタックトレースを出力する一方で制約もある。詳細はこちら

Webサーバの設定やJavaScriptが絡まないような検証であれば受入テストよりも機能テストの方が適しているとのこと。

注意事項

上記の通りだが再度念押しすると、Webサーバの制御やJavaScriptにより実現されている処理の検証には適さない。

また、Yiiについて言うと、ブラウザからアクセスした場合にはエントリスクリプトで各種設定をロードする。

yii-application/frontend/web/index.php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.php';

$config = yii\helpers\ArrayHelper::merge(
    require __DIR__ . '/../../common/config/main.php',
    require __DIR__ . '/../../common/config/main-local.php',
    require __DIR__ . '/../config/main.php',
    require __DIR__ . '/../config/main-local.php'
);

(new yii\web\Application($config))->run();

一方、codeceptコマンドで実行される機能テストでは、それと同様の設定をロードしないので、異る挙動を示す。(ちなみに受入テストではエントリスクリプトindex-test.phpで処理を行い設定をロードさせる。)

vendor/bin/codecept
#!/usr/bin/env php
<?php
/**
 * Codeception CLI
 */

require_once __DIR__.'/autoload.php';

use Codeception\Application;

$app = new Application('Codeception', Codeception\Codecept::VERSION);
$app->add(new Codeception\Command\Build('build'));
$app->add(new Codeception\Command\Run('run'));
$app->add(new Codeception\Command\Init('init'));
$app->add(new Codeception\Command\Console('console'));
$app->add(new Codeception\Command\Bootstrap('bootstrap'));
$app->add(new Codeception\Command\GenerateCept('generate:cept'));
$app->add(new Codeception\Command\GenerateCest('generate:cest'));
$app->add(new Codeception\Command\GenerateTest('generate:test'));
$app->add(new Codeception\Command\GenerateSuite('generate:suite'));
$app->add(new Codeception\Command\GenerateHelper('generate:helper'));
$app->add(new Codeception\Command\GenerateScenarios('generate:scenarios'));
$app->add(new Codeception\Command\Clean('clean'));
$app->add(new Codeception\Command\GenerateGroup('generate:groupobject'));
$app->add(new Codeception\Command\GeneratePageObject('generate:pageobject'));
$app->add(new Codeception\Command\GenerateSnapshot('generate:snapshot'));
$app->add(new Codeception\Command\GenerateStepObject('generate:stepobject'));
$app->add(new Codeception\Command\GenerateEnvironment('generate:environment'));
$app->add(new Codeception\Command\GenerateFeature('generate:feature'));
$app->add(new Codeception\Command\GherkinSnippets('gherkin:snippets'));
$app->add(new Codeception\Command\GherkinSteps('gherkin:steps'));
$app->add(new Codeception\Command\DryRun('dry-run'));
$app->add(new Codeception\Command\ConfigValidate('config:validate'));

// Suggests package
if (class_exists('Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand')) {
    $app->add(new Codeception\Command\Completion());
} else {
    $app->add(new Codeception\Command\CompletionFallback());
}

$app->registerCustomCommands();
$app->run();

例えば、PHPフレームワーク Yii の使い方で紹介したLanguageDropdownを以下のように実装している場合、上記の様に設定をロードしないと$codeにen-USが設定された結果nullを返す。

components/LanguageDropdown.php
    public static function label($code)
    {
        if (self::$_labels === null) {
            self::$_labels = [
                'ja' => '日本語',
                'en' => 'English',
            ];
        }

        return isset(self::$_labels[$code]) ? self::$_labels[$code] : null;
    }

すると、以下のコードで[yii\base\InvalidConfigException] The 'label' option is required.が発せられることになる。

views/layouts/main.php
$menuItems[] = ['label' => LanguageDropdown::label(Yii::$app->language), 'items' => LanguageDropdown::widget()];

対策としてはテストコード中で\Yii::$app->language = 'en';のように指定するなどが考えられる。

機能テストの大まかな流れ

amOnRoute()、click()やsubmitForm()などで画面上の操作を実行し、seeなんちゃらやassertなんちゃらで操作の結果を検証する。

必要に応じて、テストのスコープ等をwantTo()などで記載したり、getなんちゃらやgrabなんちゃらでテストに使用する情報を取得することもできる。

ログインのテスト

テストに利用するメソッドはdocs -> Modules -> Yii2docs -> Modules -> Assertsなどを参照。メソッド名を見ればだいたい何をやっているか想像がつく。詳細はガイドやソースコードで確認する。

tests/functional/LoginCest.php
<?php
namespace frontend\tests\functional;

// FunctionalTesterをuseしている
use frontend\tests\FunctionalTester;
use common\fixtures\UserFixture;

class LoginCest
{
    /**
     * Load fixtures before db transaction begin
     * Called in _before()
     * @see \Codeception\Module\Yii2::_before()
     * @see \Codeception\Module\Yii2::loadFixtures()
     * @return array
     */
    public function _fixtures()
    {
        return [
            'user' => [
                'class' => UserFixture::className(),
                // 予めUserモデルに合わせてテスト用ユーザーを定義しておく
                'dataFile' => codecept_data_dir() . 'login_data.php',
            ],
        ];
    }

    public function _before(FunctionalTester $I)
    {
        $I->amOnRoute('site/login');
    }

    protected function formParams($login, $password)
    {
        return [
            'LoginForm[username]' => $login,
            'LoginForm[password]' => $password,
        ];
    }

    public function checkEmpty(FunctionalTester $I)
    {
        $I->submitForm('#login-form', $this->formParams('', ''));
        $I->seeValidationError('Username cannot be blank.');
        $I->seeValidationError('Password cannot be blank.');
    }

    public function checkWrongPassword(FunctionalTester $I)
    {
        $I->submitForm('#login-form', $this->formParams('admin', 'wrong'));
        $I->seeValidationError('Incorrect username or password.');
    }

    public function checkValidLogin(FunctionalTester $I)
    {
        $I->submitForm('#login-form', $this->formParams('erau', 'password_0'));
        $I->see('Logout (erau)', 'form button[type=submit]');
        $I->dontSeeLink('Login');
        $I->dontSeeLink('Signup');
    }
}
tests/_support/FunctionalTester.php
<?php
namespace frontend\tests;

// Actorを継承している
class FunctionalTester extends \Codeception\Actor
{
    use _generated\FunctionalTesterActions;


    public function seeValidationError($message)
    {
        $this->see($message, '.help-block');
    }

    public function dontSeeValidationError($message)
    {
        $this->dontSee($message, '.help-block');
    }
}
vendor/codeception/base/src/Codeception/Actor.php
<?php
namespace Codeception;

use Codeception\Lib\Actor\Shared\Comment;
use Codeception\Lib\Actor\Shared\Friend;
use Codeception\Step\Executor;

abstract class Actor
{
    // Comment Traitをuseしている
    use Comment;
    use Friend;

    /**
     * @var \Codeception\Scenario
     */
    protected $scenario;

    public function __construct(Scenario $scenario)
    {
        $this->scenario = $scenario;
    }

    /**
     * @return \Codeception\Scenario
     */
    protected function getScenario()
    {
        return $this->scenario;
    }

    public function wantToTest($text)
    {
        $this->wantTo('test ' . $text);
    }

    public function wantTo($text)
    {
        $this->scenario->setFeature($text);
    }

    public function __call($method, $arguments)
    {
        $class = get_class($this);
        throw new \RuntimeException("Call to undefined method $class::$method");
    }

    /**
     * Lazy-execution given anonymous function
     * @param $callable \Closure
     * @return $this
     */
    public function execute($callable)
    {
        $this->scenario->addStep(new Executor($callable, []));
        $callable();
        return $this;
    }
}

expectTo()などはComment Traitに実装されている。

vendor/codeception/base/src/Codeception/Lib/Actor/Shared/Comment.php
<?php
namespace Codeception\Lib\Actor\Shared;

trait Comment
{
    /**
     * @return \Codeception\Scenario
     */
    abstract protected function getScenario();

    public function expectTo($prediction)
    {
        return $this->comment('I expect to ' . $prediction);
    }

    public function expect($prediction)
    {
        return $this->comment('I expect ' . $prediction);
    }

    public function amGoingTo($argumentation)
    {
        return $this->comment('I am going to ' . $argumentation);
    }

    public function am($role)
    {
        $role = trim($role);

        if (stripos('aeiou', $role[0]) !== false) {
            return $this->comment('As an ' . $role);
        }

        return $this->comment('As a ' . $role);
    }

    public function lookForwardTo($achieveValue)
    {
        return $this->comment('So that I ' . $achieveValue);
    }

    public function comment($description)
    {
        $this->getScenario()->comment($description);
        return $this;
    }
}

機能テストと受入テストで利用できるテストコードの再利用の方法はこちら。ログインのテストの再利用について紹介されている。

入力フォームのテスト

tests/functional/SignupCest.php
<?php
namespace frontend\tests\functional;
use frontend\tests\FunctionalTester;

class SignupCest
{
    // クリックするsubmitボタンのidを指定する
    protected $formId = '#form-signup';

    public function _before(FunctionalTester $I)
    {
        // テスト対象のページに遷移する
        $I->amOnRoute('site/signup');
    }

    public function signupWithEmptyFields(FunctionalTester $I)
    {
        $I->see('Signup', 'h1');
        $I->see('Please fill out the following fields to signup:');
        // フォームに何も入力せずに送信する
        $I->submitForm($this->formId, []);
        // エラーになることを確認する
        $I->seeValidationError('Username cannot be blank.');
        $I->seeValidationError('Email cannot be blank.');
        $I->seeValidationError('Password cannot be blank.');
    }

    public function signupWithWrongEmail(FunctionalTester $I)
    {
        $I->submitForm(
            $this->formId, [
            // フォームを入力する(不適切な入力)
            'SignupForm[username]'  => 'tester',
            'SignupForm[email]'     => 'ttttt',
            'SignupForm[password]'  => 'tester_password',
            ]
        );
        // 未入力エラーではないことを確認する
        $I->dontSee('Username cannot be blank.', '.help-block');
        $I->dontSee('Password cannot be blank.', '.help-block');
        // 入力内容が不適切であることを確認する
        $I->see('Email is not a valid email address.', '.help-block');
    }

    public function signupSuccessfully(FunctionalTester $I)
    {
        // SignupFormモデルとUserモデルも確認すると理解が深まる 

        $I->submitForm($this->formId, [
            // フォームを入力する(適切な入力)
            'SignupForm[username]' => 'tester',
            'SignupForm[email]' => 'tester.email@example.com',
            'SignupForm[password]' => 'tester_password',
        ]);

        // ユーザが存在することを確認する
        $I->seeRecord('common\models\User', [
            'username' => 'tester',
            'email' => 'tester.email@example.com',
            'status' => \common\models\User::STATUS_INACTIVE
        ]);

        // メールが送信されたことを確認する
        $I->seeEmailIsSent();
        // フォーム送信が正しく行わた時の画面であることを確認する
        $I->see('Thank you for registration. Please check your inbox for verification email.');
    }
}

メールのテスト

// メールが送信できたかどうかを検証する。
$I->seeEmailIsSent();

// メールの内容を取得する。
$message = $I->grabLastSentEmail();

// メールの内容を検証する。
$I->assertEquals('admin@site,com', $message->getTo());

受入テスト

受入テストを実行する前に環境構築しておかないとエラーが発生する。

環境構築

YiiのテストのクイックスタートガイドSelenium 環境構築を参考に環境をつくる。

概ね以下のような感じ。

$ cd /somewhere/yii-application
$ mv frontend/tests/acceptance.suite.yml.example frontend/tests/acceptance.suite.yml
$ vendor/bin/codecept build -- -c frontend

Seleniumの環境構築に関して、Webドライバのインストールは以下のような感じ。

$ cd /somewhere/yii-application
$ vi composer.json
$ php composer.phar require --dev facebook/webdriver
yii-application/composer.json
    "require-dev": {
        "facebook/webdriver": "^1.0.1",

環境構築が済んだらSeleniumサーバを起動する。

$ ls /somewhere/yii-application/vendor/facebook/
webdriver

$ ls /opt/selenium/
selenium-server-standalone-3.141.59.jar

$ ls /usr/local/bin
chromedriver  geckodriver

$ java -jar /opt/selenium/selenium-server-standalone-3.141.59.jar
yii-application/frontend/tests/acceptance.suite.yml
suite_namespace: frontend\tests\acceptance
actor: AcceptanceTester
modules:
    enabled:
        - WebDriver:
            # URL、ポート番号は環境に応じて書き換える
            url: http://localhost:8080
            # ブラウザドライバを指定する
            browser: chrome
        - Yii2:
            part: 
                - init
                - fixtures

エラー事例

エラーメッセージ 原因
Suite 'acceptance' could not be found acceptance.suite.yml.exampleをリネームしていないなど環境構築が未済
WebDriver: Class Facebook\WebDriver\Remote\RemoteWebDriver can't be loaded, please add "facebook/webdriver": "^1.0.1" to composer.json WebDriverがインストールされていない
[ConnectionException] Can't connect to Webdriver at http://127.0.0.1:4444/wd/hub. Please make sure that Selenium Server or PhantomJS is running. Seleniumサーバが起動していない
[yii\base\ErrorException] Undefined index: ELEMENT ブラウザドライバ(geckodriver)のバグ?

ドライバ周辺の不具合についてはバージョンを変えるなり、chrome driverなど別のドライバに変えると解消することがある。

テストの書き方

機能テスト同様CESTの様式で書くが、利用できるメソッドに違いがある。ブラウザには、PhpBrowserとWebDriver(+ブラウザ)があり、用途に応じて使い分ける。wait系のメソッドはWebDriverに特有の機能。詳細はこちら

$ vendor/bin/codecept run acceptance HomeCest -- -c frontend

DBを使用したテスト

DB Moduleを利用できるように設定する。

yii-application/frontend/tests/acceptance.suite.yml
modules:
    enabled:
        - Db:
            dsn: "mysql:host=localhost;dbname=yii2advanced_test"
            user: "yii2advanced_test"
            password: "xxxx"

テストコードを書き、実行する。

tests/acceptance/LoginCest.php
<?php
namespace frontend\tests\acceptance;

use frontend\tests\AcceptanceTester;
use yii\helpers\Url;
use common\fixtures\UserFixture;

class LoginCest
{
    public function _fixtures()
    {
        return [
            'user' => [
                'class' => UserFixture::className(),
                'dataFile' => codecept_data_dir() . 'user.php',
            ],
        ];
    }

    public function _before(AcceptanceTester $I)
    {
        $I->amOnPage(Url::toRoute('/site/login'));
        $I->wait(1);
    }

    protected function formParams($login, $password)
    {
        return [
            'LoginForm[username]' => $login,
            'LoginForm[password]' => $password,
        ];
    }

    public function checkValidLogin(AcceptanceTester $I)
    {
        $I->see('Login', 'h1');

        $I->seeInDatabase('user', ['erau' => 'sfriesen@jenkins.info']);
        $I->submitForm('#login-form', $this->formParams('erau', 'password_0'));
        $I->wait(1);

        $I->see('Logout (erau)', 'form button[type=submit]');
        $I->dontSeeLink('Login');
        $I->dontSeeLink('Signup');
    }
}

注意事項

Pretty URLを適用している場合、.htaccessindex.phpで処理させるように書いてあるためindex-test.phpで処理されない。よってテスト用の設定(config/test.phpなど)が読み込まれない。結果、テスト用に作成したデータベースに接続されない。

対策は少なくとも3通りある。

  • index.phpで読み込む設定ファイル中でテスト用のDBに接続する。
  • .htaccessを書き換えてindex-test.phpで処理させる。
  • index.phpindex-test.phpを統合し、環境に応じてYII_ENVを書き換える。(下記)
yii-application/frontend/web/index.php
<?php
// NOTE: Make sure this file is not accessible when deployed to production
// 適宜IPアドレスを追加する
if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
    die('You are not allowed to access this file.');
}

defined('YII_DEBUG') or define('YII_DEBUG', true);
// 受入テストを行う環境ではYII_ENVにtestを設定する
defined('YII_ENV') or define('YII_ENV', 'test');

require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.php';

if (YII_ENV === 'test') {
    $config = yii\helpers\ArrayHelper::merge(
        require __DIR__ . '/../../common/config/main.php',
        require __DIR__ . '/../../common/config/main-local.php',
        require __DIR__ . '/../../common/config/test.php',
        require __DIR__ . '/../../common/config/test-local.php',
        require __DIR__ . '/../config/main.php',
        require __DIR__ . '/../config/main-local.php',
        require __DIR__ . '/../config/test.php',
        require __DIR__ . '/../config/test-local.php'
    );
} else {
    $config = yii\helpers\ArrayHelper::merge(
        require __DIR__ . '/../../common/config/main.php',
        require __DIR__ . '/../../common/config/main-local.php',
        require __DIR__ . '/../config/main.php',
        require __DIR__ . '/../config/main-local.php'
    );
}

(new yii\web\Application($config))->run();

Tips

cliでステップ実行する

vendor/bin/codeceptを真似たcli_debug.phpを作成し、IDEのデバッグ機能でcli_debug.php run unit -- -c frontendのようにして実行する。

yii-application/cli_debug.php
<?php
/**
 * Codeception PHP script runner
 */
require_once dirname(__FILE__).'/vendor/codeception/base/autoload.php';

use Codeception\Application;

$app = new Application('Codeception', Codeception\Codecept::VERSION);
$app->add(new Codeception\Command\Build('build'));
$app->add(new Codeception\Command\Run('run'));
$app->add(new Codeception\Command\Init('init'));
$app->add(new Codeception\Command\Console('console'));
$app->add(new Codeception\Command\Bootstrap('bootstrap'));
$app->add(new Codeception\Command\GenerateCept('generate:cept'));
$app->add(new Codeception\Command\GenerateCest('generate:cest'));
$app->add(new Codeception\Command\GenerateTest('generate:test'));
$app->add(new Codeception\Command\GenerateSuite('generate:suite'));
$app->add(new Codeception\Command\GenerateHelper('generate:helper'));
$app->add(new Codeception\Command\GenerateScenarios('generate:scenarios'));
$app->add(new Codeception\Command\Clean('clean'));
$app->add(new Codeception\Command\GenerateGroup('generate:groupobject'));
$app->add(new Codeception\Command\GeneratePageObject('generate:pageobject'));
$app->add(new Codeception\Command\GenerateSnapshot('generate:snapshot'));
$app->add(new Codeception\Command\GenerateStepObject('generate:stepobject'));
$app->add(new Codeception\Command\GenerateEnvironment('generate:environment'));
$app->add(new Codeception\Command\GenerateFeature('generate:feature'));
$app->add(new Codeception\Command\GherkinSnippets('gherkin:snippets'));
$app->add(new Codeception\Command\GherkinSteps('gherkin:steps'));
$app->add(new Codeception\Command\DryRun('dry-run'));
$app->add(new Codeception\Command\ConfigValidate('config:validate'));

// Suggests package
if (class_exists('Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand')) {
    $app->add(new Codeception\Command\Completion());
} else {
    $app->add(new Codeception\Command\CompletionFallback());
}

$app->registerCustomCommands();
$app->run();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CentOS 7にPHP 7.2をインストール(Remi's RPM repository)

はじめに

Remi's RPM repositoryを利用してCentOS7にPHP7.2をインストール
親記事:PHP EOLと各種インストール方法
参考:Remi's RPM repository

サポート

本手法で導入した場合、PHP: Supported Versions/PHP: Unsupported Branchesより、2020-11-30がEOLになると思われる。
それ以降に報告された脆弱性への対応は実施されない可能性がある。

note

  • インストール後の更新は yum --enablerepo=remi-php72 update

LOG

インストール

# cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)

# yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
... 略

# yum install -y --enablerepo=remi-php72 php which
... 略

各種確認

# which php
/usr/bin/php

# php -v
PHP 7.2.23 (cli) (built: Sep 25 2019 07:38:48) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

# php -i | grep php.ini
Configuration File (php.ini) Path => /etc
Loaded Configuration File => /etc/php.ini

# yum info php
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
 * base: ftp.yz.yamagata-u.ac.jp
 * epel: nrt.edge.kernel.org
 * extras: ftp.yz.yamagata-u.ac.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp.yz.yamagata-u.ac.jp
Installed Packages
Name        : php
Arch        : x86_64
Version     : 7.2.23
Release     : 1.el7.remi
Size        : 10 M
Repo        : installed
From repo   : remi-php72
Summary     : PHP scripting language for creating dynamic web sites
URL         : http://www.php.net/
License     : PHP and Zend and BSD and MIT and ASL 1.0 and NCSA
Description : PHP is an HTML-embedded scripting language. PHP attempts to make it
            : easy for developers to write dynamically generated web pages. PHP also
            : offers built-in database integration for several commercial and
            : non-commercial database management systems, so writing a
            : database-enabled webpage with PHP is fairly simple. The most common
            : use of PHP coding is probably as a replacement for CGI scripts.
            :
            : The php package contains the module (often referred to as mod_php)
            : which adds support for the PHP language to Apache HTTP Server.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】LengthAwarePaginatorで1つのページに複数ページネーションを実装 -Laravel

LengthAwarePaginatorを使って1つのページに複数ページネーションを実装。

参考
Laravel5で1ページに複数のPaginationを実装する
https://qiita.com/sano1202/items/371a59ea46cb66993b8b

こちらを参考にさせていただきました!

が、LengthAwarePaginatorを使用して複数ページネーションを実装しようとした際に詰まったので備忘録。

Paginationのパラメータ名を変更する

パラメータ名が変更できずに随分困りましたが第5引数に

['pageName' => 'hogehoge']

を入れるだけでした。。

$players = new LengthAwarePaginator($list , $all_num, $disp_limit, $page, ['pageName' => 'hogehoge']);

これで、user/index?hogehoge=2
のようになります。

初心者なので、、こんなのもわからないの?!と言わずお手柔らかにmm
備忘です。

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

【Laravel】LengthAwarePaginatorで1つのページに複数ページネーションを実装 -Laravel5

LengthAwarePaginatorを使って1つのページに複数ページネーションを実装。

参考
Laravel5で1ページに複数のPaginationを実装する
https://qiita.com/sano1202/items/371a59ea46cb66993b8b

基本的にはこちらを参考にさせていただきました!

が、LengthAwarePaginatorを使用して複数ページネーションを実装しようとした際に詰まったので備忘録。

Paginationのパラメータ名を変更する

パラメータ名が変更できずに随分困りましたが第5引数に

['pageName' => 'hogehoge']

を入れるだけでした。。

$users = new LengthAwarePaginator($users , $count, $per_page, $page, ['pageName' => 'hogehoge']);

$users->setPath(url('/users'));

これで、/users?hogehoge=2
のようになります。

初心者なので、、こんなのもわからないの?!と言わずお手柔らかにmm
備忘です。

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

Laravelでフォーム保存後のフィードバックにSessionのフラッシュデータを使うのと、そのテスト方法

Webページでは何らかのフォームで保存ボタンを押した後に次のページにリダイレクトすることがあり、
リダイレクトされた次のページでフィードバックしたいということがよくあります。

image.png
※画像はイメージです

Laravelにおいてこのフィードバックを出す方法と、そのテスト方法について説明します。

バックエンドでセッションのフラッシュデータに値を入れる

Controller で以下のようにwithでフラッシュデータに値を入れておきます。

 public function add() {
   // 保存とか色々する
   // 最後に次のページへリダイレクトさせる
   return redirect()->route('/')->with('success', '保存が成功しました');
 } 

公式ドキュメントでいうと https://readouble.com/laravel/5.8/ja/responses.html

フラッシュデータを保存するリダイレクト

あたりの章です。

フィードバックの HTML と SCSS

Laravelのヘルパ関数でsession()というのがあるので、それで値をとります。

LaravelのBladeファイルで書くとHTMLは以下のようになります。

@if(session('success'))
    <div class="feedback-wrapper">
        <div class="feedback success">
            {{ session('success') }}
        </div>
    </div>
@endif

対応するSCSS

.feedback-wrapper {
  .feedback {
    margin: auto;
    padding: 12px;
    border-radius: 4px;
    text-align: center;

    &.success {
      border: 1px solid green;
      color: green;
      font-weight: bold
    }
  }
}

テスト方法

Featureテストを書く時に、フラッシュデータが何が入っているかというのをテストに書きます。

    public function testAdd()
    {
        $this->post('/test/add', [
            'text' => '入力したテキストです',
        ])->assertRedirect('/text')->assertSessionHas('success');
    }

assertSessionHas() を使えば、仮にリダイレクト先が同じでも、入っているフラッシュデータが違う 'success' と 'warning' であっても区別することができます。

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

PHP Slim 環境構築(8) S3

PHP Slim 環境構築(8) S3

Introduction

前回は、ローカルDynamoDBにアクセスしてみました。
今回は、ローカル用のS3互換サービスMinIOを使って、S3へのアクセスを行います。

MinIO

MinIOは、エンタープライズレベルのオブジェクトストレージサービスです。
・・・なんですが、手軽に使えるS3互換ストレージとして、ローカル開発環境によく使用されているようです。

変更点

ソースツリー

前回からの変更・追加ソースは以下の通りです。

$(PROJECTROOT)
  /compose
    docker-compose.yml
  /src
    /hoge
      /lib
        /Controller
          StorageController.php            (NEW!)
        /Model
      /public
        index.php

docker-compose.yml

新たにS3互換のminIO用コンテナを追加します。

なお、この記事を書いている途中でdocker-composeのlinksオプションはすでに時代遅れなのを知ってしまったので、今回からlinksは全て削除しています。すでにnetworksを使っているので、linksは冗長だったんですね。

  ..(略)..  
  storage:
    image: minio/minio
    volumes:
      - storage-data:/data
    container_name: storage
    ports:
      - "19000:9000"
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: miniminio
    command: server /data
    networks:
      - local_net

index.php

DIにstorage用の設定とstorageオブジェクトの定義を行い、storage用のrouteを追加します。

/src/hoge/public/index.php
..()..  

use Hoge\Controller\RedisController;
use Hoge\Controller\DynamodbController;
use Hoge\Controller\StorageController;                   (追加)

..()..  

$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
    'settings' => [

        ..()..  

        'storage' => [
            'endpoint' => 'http://storage:9000',
            'region' => 'ap-northeast-1'
        ]
    ],

    ..()..  

    'storage' => function (ContainerInterface $container) {
        $settings = $container->get('settings')['storage'];
        $sdk = new Aws\Sdk([
            'endpoint' => $settings['endpoint'],
            'region' => $settings['region'],
            'version' => '2006-03-01',
            'credentials' => [
                'key' => 'minio',
                'secret' => 'miniminio'
            ],
            //'bucket_endpoint' => true,
            'use_path_style_endpoint' => true
        ]);
        $s3 = $sdk->createS3();
        return $s3;
    }

..()..  

$app->group('/storage', function (RouteCollectorProxy $group) {
    $group->get('/{filename}', StorageController::class . ':get');
    $group->post('', StorageController::class . ':post');
});

$app->run();



### StorageController

ファイルのアップロードとファイルの表示(ダウンロードにするためにはcontent-typeとかいじればok)に対応しています


```php:/src/hoge/lib/Controller/StorageController.php
<?php

namespace Hoge\Controller;

use Aws\S3\Exception\S3Exception;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Container\ContainerInterface;

class StorageController
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;

        /** @var \Aws\S3\S3Client $s3 */
        $s3 = $this->container->get('storage');
        try {
            // バケット確認
            $s3->headBucket(['Bucket' => 'dummy']);
        } catch (S3Exception $ex) {
            $errorCode = $ex->getAwsErrorCode();
            if ($errorCode === 'NotFound') {
                // バケットが無ければ作る
                $s3->createBucket([
                    'Bucket' => 'dummy',
                    'CreateBucketConfiguration' => [
                        'LocationConstraint' => 'ap-northeast-1'
                    ]
                ]);
            }
        }
    }

    public function get(Request $request, Response $response, array $args) : Response
    {
        // parameters (id)
        $key = $args['filename'];

        /** @var \Aws\S3\S3Client $s3 */
        $s3 = $this->container->get('storage');

        try {
            $result = $s3->getObject([
                'Bucket' => 'dummy',
                'Key' => $key
            ]);
        } catch (S3Exception $ex) {
            return $response->withStatus(404);
        }

        $body = $result->get('Body');
        $newResponse = $response->withBody($body);
        return $newResponse;
    }

    public function post(Request $request, Response $response, array $args) : Response
    {
        $files = $request->getUploadedFiles();
        /** @var \Psr\Http\Message\UploadedFileInterface $uploadedFile */
        $uploadedFile = $files['upload'];

        $err = $uploadedFile->getError();
        if ($err !== 0) {
            return $response->withStatus(500);
        }
        $clientFileName = $uploadedFile->getClientFilename();

        /** @var \Aws\S3\S3Client $s3 */
        $s3 = $this->container->get('storage');

        $result = $s3->putObject([
            'Bucket' => 'dummy',
            'Key' => $clientFileName,
            'Body' => $uploadedFile->getStream()
        ]);
        $url = $result->get('ObjectURL');

        $response->getBody()->write($url);
        return $response;
    }
}

テスト

wgetでpost fileuploadをテストするために、以下のようなコマンドを実行しました。

postdata.txt
--cuthere
Content-Disposition: form-data; name="upload"; filename="videodrome.txt"
Content-Type: text/plain

THIS IS YOUR TAPE.
--cuthere--
$ wget http://hoge.localhost/.storage --header='Content-Type: multipart/form-data; boundary=cuthere' --post-file=postdata.txt

ここまでのソース

こちらでどうぞ。

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

Laravelでレコード生成、更新時に自動更新されるタイムスタンプをミリ秒にする

概要

Laravelでレコード生成、更新時に自動更新されるタイムスタンプをミリ秒にする。

実装

モデルクラス
class モデル名 extends Model
{
    // このメソッドをオーバーライドする
    public function getDateFormat()
    {
        return 'Y-m-d H:i:s.u';
    }
}
マイグレートクラス
class マイグレート名 extends Migration
{
    public function up()
    {
        Schema::create('table_name', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->dateTime('created_at', 6); // 精度を6にする。
            $table->dateTime('updated_at', 6); // 精度を6にする。
        });
    }
}

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

【FuelPHP】クエリビルダでSQLを出力する

メモとして残します。

■やり方

$query_builder = \DB::select('col')->from(['table','tbl']);
//上記のビルダオブジェクトのSQLを出力
echo $query_builder->compile();
// 出力内容 SELECT `col` FROM `table` AS tbl;

サブクエリをFROMで指定するときなどで活躍。

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

サーバアプリケーションのログを取る方針、脳死でも出来る事

ログを取る時に考える事

PHP, Laravel を今扱ってるからとりあえず前提は PHP, Laravel で。
他の環境の時も別にやる事は変わらないと思う。

サーバ業務をやってると幸運な事に色々な設計を任せて貰えるんだけど、自分が直接担当しない時にログどうしましょうかと相談されたので、そういう時に伝える基本方針を書いておく。

というか、何も知らない人は頼むからこれだけは最低限やっておいてくれって感じの内容 (そこも人によって違うからあれだけど参考にして損はないはず)。

ログとは

ログと一括りにすると色々あるだろと突っ込みを貰う事になるので、ここではアプリケーション保守を念頭に置いたログの事を指す。
それも (ry みたいな話はあれど、OS に踏み込んだりはせず、サーバサイドアプリケーションエンジニア (長い...) が気にするポイントを書きだしておけば迷いは消える筈。

アクセスログを取る

別に PHP に限らない話になってきそうな気がすでにしてるけど、一旦無視して、前提として HTTPD (Apache や Nginx など) のアクセスログは取る。

アプリケーションログを取る

API のリクエスト時に送信されてくる POST 等のデータ変更系のメソッドは、リクエストデータ加工前にログを吐いておく。
また、レスポンスは有無を言わさず全て取る。

GET, POST, PUT, DELETE はそれぞれ取るようにしておく (POST で投げて _method: 'delete' を DELETE として扱うようなフレームワークの場合、そこの考慮も忘れずやる)。
パラメータは全て保存と言いたい所だけど、クレジットカードなどの平文保存がまずいものに関しては任意のフィールドをマスク (番号を xxxxx... と他の文字列で置き換えて戻せなくする) する事ができる仕組みを入れておく。

いつ誰がサーバに訪問したか、どういうリクエストを投げたのかという情報はサービス全ての起点になるため、これを取らなかったりすると調査が不可能になったりする。

ログレベルは INFO で脳死 (プロジェクト的に理由があって変える場合は違うログレベルで吐く)。

DB 接続とかその多詳細なプログラムレベルのログについて

DB 接続エラーとか、個々の要件毎のエラーケースみたいなものはサービスレベルでどういうログが欲しいとかがあるし、この変は脳死では対応できないので、深刻度に合わせて話して決めておく。

この辺りは INFO というよりはもっと上の深刻度の高いログの為、適切な対応が必要 (本当に脳死は良くない)。
ただ、そういう書き方をすると何も設定しない人がでてきたりするので、\Exception で catch して error 詳細と使ったパラメータを吐くくらいは入れとくと良い (そのままエラーを握り潰さないようにとりあえず同じ例外を再度 throw しておく)。

DEBUG を上手く使う

少し余談。

開発していると皆 dd 挟んだりブレークポイント入れたり工夫してデバッグするけど、止めて眺める系は作業効率は良くなかったりするし、一連のデータを俯瞰して見る事に適さないので、log で吐く事を覚えると幸せになれる。

不要なコードやログを入れたくないという意見がたまにあるけど、そもそもそれは DEBUG が担う部分なので、気兼ねなく Log::DEBUG を仕込むといい。
特に共通化しているような処理 (Model レベルでのデータ整形や Service 層、Job とか視認しづらい部分は特に) に前提にしているデータと加工後のデータや返却したデータなどを吐いておくとどこでおかしくなってるかが分かるようになるのでバンバン差し込んでおくこと。

ログローテーションは必ず動作確認する

未設定の場合は必ず設定する事。
吐く場所がどこかによって変わるけど、ボリュームの空きを使いきったら (usage 100% とか) サーバが動かなくなったり心あたりの無い挙動をする事になるので。
多分監視とか入れるだろうから基本的には大丈夫だと思うけど、たまにログで使いきって死ぬサーバがいる (そもそも log のボリューム分けてないのかという話はあるが、あくまでもリスクコントロールの話)。

またローテーション設計はログの回収、バックアップ頻度、ボリュームサイズなどインフラ依存のため、担当と相談して決める。

この辺りは使ってるフレームワークでローテーションが動いてる事を確認するのと、設定、変更方法を確認しておく (後から変更が難しい場合や、変更箇所が多い場合は先に決めてしまう)。

最後に

書いてると全部脳死って訳にはいかないなーという感想になったけど、とりあえず細かい所は置いておいて、最低ないとやばいのは書けたと思う。

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

名前空間の意義

名前空間とは

別ファイルの関数やviewファイルを読み込みたいときはrequire()やinclude()を使用することで、そのファイルの中身を利用することができる。

上記方法では全部の内容を読み込むが、名前空間を利用することでクラス名のみを同じように使うことができる。

名前空間については以前に投稿したが、改めて復習すると、重複するクラス名やメソッド名を同じ名前であったとしても、使うことができる概念である、と説明した。

というのも、ファイルが多くなってくるとどの関数名やクラス名を使用したか分からなくなってしまうので、それを気にせずに利用したいときに非常に便利である。

例えば以下のような書き方ができる。

namespace dirA\dirB\dirC(現在ディレクトリ)

use dirD\...\クラス名X
use dirE\...\クラス名Y

上は何を示しているかというと、
まず、利用したいクラスが書かれているファイルがdirA/dirB/dirCの下のDとEのさらに下にあるとした場合、
2つのクラスのファイルの上流のディリレクトリをnamespaceで宣言する。
こうすることで、共通のディレクトリはuse以降に書かなくて済む。

そして、useにはそれぞれ利用したいクラスがあるファイルまでのディレクトリと最後にクラス名を書くことでそのクラス名をこのファイルの中で使うことができる。

補足だが、オートローダーを利用することで、クラスをuseしなくても利用する方法がある。これについては、別の機会に解説する。

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

Null 合体演算子(??)と三項演算子(?:)の空白('')の結果の違いについて

以下のような違いがあります。
- Null 合体演算子(??)では、空白('')は true 扱いになる
- 三項演算子(?:)では、空白('')は false 扱いになる

>>> '' ?? false
=> ""
>>> '' ?: false
=> false
>>> '' ? true : false
=> false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Null 合体演算子(??)とエルビス演算子(?:)と三項演算子(A?B:C)の空白('')の結果の違いについて

以下のような違いがあります。
- Null 合体演算子(??)では、空白('')は true 扱いになる
- エルビス演算子(?:)と三項演算子(A?B:C)では、空白('')は false 扱いになる

>>> '' ?? false
=> ""
>>> '' ?: false
=> false
>>> '' ? true : false
=> false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む