20200205のPHPに関する記事は11件です。

備忘録_レンタル共用サーバーでcomposerを使う

  1. 以下のリンクを参考に、composerインストール用のPHPファイルを作成
    ※諸事情によりphp -rが実行できん場合に限る
    https://getcomposer.org/download/

    <?php
    
    // composerインストール用ファイルをダウンロード
    copy('https://getcomposer.org/installer', 'composer-setup.php');
    
    // hashの確認
    // ※これをやる意義が明確にわかっていないし、必要性もわかっていない。
    // ※オフィシャルに書いてあるからやっておく。
    $hash = 'c5b9b6d368201a9db6f74e2611495f369991b72d9c8cbd3ffbc63edff210eb73d46ffbfce88669ad33695ef77dc76976';
    if (hash_file('sha384', 'composer-setup.php') !== $hash) {
        echo('Installer corrupt');
        unlink('composer-setup.php');
        return;
    }
    
    // composerインストール用ファイルを実行
    include('composer-setup.php');
    
  2. 作成したphpファイルをコマンドラインから起動

    php {上記「1」で作成したファイル名}
    
  3. composerのPATHを通す

    mv composer.phar ~/env/bin/composer
    PATH=$PATH:~/env/bin
    
  4. composerのインストール完了!
    ※しかし、SSHログアウトするたびにPATHが初期化される

  5. レンタル共用サーバー使うのやめるわ!(❁´◡`❁)

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

【PHP】配列を文字列(EDI情報)化

はじめに

EDI情報作成時に使ったロジックなので備忘録として残します。

サンプルコード

class TestA
{
    public function __construct()
    {
        $this->test1 = "00";
        $this->test2 = "          ";
        $this->test3 = "00";
        $this->test4 = "          ";
        $this->test5 = "GG";
    }
    public function getTest1()
  {
      return $this->test1;
  }
  public function setTest1($test1)
  {
      $this->test1 = $$test1;

      return $this;
  }
    ・・・(省略)
}


class TestB
{
    $test;

    public function build()
    {
        $this->setTest(new TestA());
        $test = $this->getTest();

        $testMessage = 'AAA'.'|'.
        $test->getTest1().'|'.
        $test->getTest2().'|'.
        $test->getTest3().'|'.
        $test->getTest4().'|'.
        $test->getTest5();
        var_dump($testMessage);
    }

    public function getTest()
    {
        return $this->test;
    }

    public function setTest()
    {
        $this->test = $test;

        return $test;
}

var_dumpの結果

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

DoctrineCacheBundleが非推奨になったのでSymfonyのCacheを使うようにした

概要

Symfonyのアプリケーションで、キャッシュをする際に DoctrineCacheBundle を使っていたが、
doctrine-bundle が2.0系にバージョンアップしたタイミングで、
DoctrineCacheBundle が非推奨になっていたので、
SymfonyのCacheで実装したのと、その際に クリアキャッシュで少しハマった話。

環境

  • Docker
    • Symfonyアプリケーションが動作するWebコンテナ(:443, :80)
      • Apache 2.4.41
      • PHP 7.3.9
      • Symfony 4.4.3
      • PHPUnit 8.5.2
    • DBコンテナ(:3306)
      • MySQL 5.6
    • Memcachedコンテナ(:11211)

実装方法

doctrine-bundle パッケージが 2.0系にバージョンアップしたタイミングで非推奨になった。

https://github.com/doctrine/DoctrineBundle/blob/2.0.6/UPGRADE-2.0.md#deprecation-of-doctrinecachebundle

Deprecation of DoctrineCacheBundle
Configuring caches through DoctrineCacheBundle is no longer possible. Please use symfony/cache through the pool type or configure your cache services manually and use the service type.

Symfony/cache を使えと書いてあるので今回はそれに従って修正した :pencil:

実装としてはこんな感じ

# framework.yaml
framework:
    cache:
        pools:
            app_memcached:
                public: true
                adapter: cache.adapter.memcached
                provider: app_memcached_provider

HogeというクラスにキャッシュをDIさせる

# services.yaml
parameters:
    memcached_servers:
        - 'memcached://memcached:11211' # 環境によって複数台書いたり

services:
    app_memcached_provider:
        class: \Memcached
        factory: ['Symfony\Component\Cache\Adapter\MemcachedAdapter', 'createConnection']
        arguments:
            - "%memcached_servers%"

    hoge:
        class: Hoge
        arguments:
            - "@app_memcached"

使い方はこんな感じ(関数名とか動作内容はテキトー)

..
class Hoge
{
    /** @var CacheInterface */
    private $cache;

    public function __construct(
        CacheInterface $cache
    ) {
        $this->cache = $cache;
    }

    /**
     * キャッシュされていればtrue キャッシュされていなければ保存してfalseを返す
     * @param string $key
     * @return bool
     */
    public function isCached(string $key): bool
    {
        /** @var CacheItem $item */
        $item = $this->cache->getItem($key);

        if ($item->isHit()) {
            return true;
        }

        $item->set(true);
        $item->expiresAfter(3600);
        $this->cache->save($item);
        return false;
    }

これを検証するために書いたテスト。いたってシンプル。

..
class HogeTest extends WebTestCase
{
    use KernelTestTrait;

    public function setUp(): void
    {
        static::bootKernel();

        // mt_srand(1); // この乱数のシード値を設定するとテストが落ちる。なぜでしょう。
        $cache = $this->getContainer()->get('app_memcached');
        $cache->clear(); // テストの前にはキャッシュをクリアさせる
    }

    public function test_キャッシュが既にされていればTrueがかえること()
    {
        $hoge = $this->getContainer()->get('hoge');
        // 一旦キャッシュさせる
        $hoge->isCached('key');
        $this->assertTrue($hoge->isCached('key'));
    }

    public function test_キャッシュしてなければ新規にキャッシュされてFalseがかえること()
    {
        $hoge = $this->getContainer()->get('hoge');
        $this->assertFalse($hoge->isCached('key'));
    }
}

コメントに書いたように、SymfonyのCacheを使うようにしたことによって、ハマったというのが、

この キャッシュをクリアする前に乱数のシード値を設定しているとキャッシュがクリアされない という問題。

イメージでは、 $cache->clear() を実行すれば、キャッシュ内は全てクリアされるものだと思っていたが、

Symfony\Component\Cache\Traits\AbstractTrait.php を確認すると、

https://github.com/symfony/symfony/blob/v4.4.3/src/Symfony/Component/Cache/Traits/AbstractTrait.php#L112-L136

どうやら ネームスペースを更新することで実質キャッシュクリア をしている模様。 実質。

versioningIsEnabled が TRUE である場合

そして、その ネームスペースの更新は乱数を元に生成されていること

つまり、キャッシュをクリアする前に乱数のシード値を固定値で設定してしまうと、その後 $cache->clear() をしても、同じネームスペースが作成されるため、実質キャッシュクリアができておらず、前のテストで実行したキャッシュが残っており、意図した挙動とならない。

Symfonyキャッシュクリアのイメージ

image_symfony_cache.png

自分の場合は、テストデータを作成するパッケージが内部で乱数のシード値を設定していたために発生していた。

実際にキャッシュの中身を確認すると、ネームスペースがキャッシュのキーに含まれていることが分かる。

telnet memcached 11211
..
Connected to memcached.
Escape character is '^]'.

stats items
STAT items:1:number 16
..
stats cachedump 1 100
ITEM ivGyRuQzLa%3AXiCMZ%3Akey [4 b; 1580539034 s]
..
get ivGyRuQzLa%3AXiCMZ%3Akey
VALUE ivGyRuQzLa%3AXiCMZ%3Akey 0 4
b:1;
END

キャッシュのキーが ivGyRuQzLa:XiCMZ:key で 間の XiCMZ がネームスペースのバージョン。

もちろん $cache->clear() してもexpiredしない限り残り続けている。

回避策としては、

  • キャッシュクリアの前に、乱数の初期化をする
    • mt_srand()
  • もしくは、テストで検証したいキャッシュのキーが分かっているのであれば、明示的に消してからテストを実行する
    • $cache->delete('key')
    • 意図せず別のキーでキャッシュされてしまい、他のテストに影響を及ぼす可能性はある

他の人はどうやっているのか気になるところ。

  

今回、MemcachedAdapterでしか動作確認してませんが、RedisAdapterも同様に AbstractAdapterを継承しているので、clearの挙動は同じかもです。

そもそも、キャッシュの部分を検証したい内容に含めるかどうか(モックで代用する)とか考えてもいいかもしれない。

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

【WebAudioAPI】録音した音声をバイナリデータ化、PHPへ受け渡し

概要

Node.js上で、IBMのWatsonによって人が話した音声データを自動で文字起こしするスクリプトを作成しました。
その中で、結構苦労した
PCのマイクに直接アクセス→録音した音声データをバイナリデータ化、PHPへ受け渡し
の部分をメモがてら貼り付け。

環境

$php -v
PHP 7.1.23 (cli) (built: Feb 22 2019 22:19:32) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies

録音部分

hogehoge.js
// 音声データのバッファをクリアする
    audioData = [];

     //様々なブラウザでマイクへのアクセス権を取得する
    navigator.mediaDevices = navigator.mediaDevices || navigator.webkitGetUserMedia;

    //audioのみtrue。Web Audioが問題なく使えるのであれば、第二引数で指定した関数を実行
    navigator.getUserMedia({
        audio: true,
        video: false
    }, successFunc, errorFunc);

    function successFunc(stream) {
        const audioContext = new AudioContext();
        sampleRate = audioContext.sampleRate;

        // ストリームを合成するNodeを作成
        const mediaStreamDestination = audioContext.createMediaStreamDestination();

        // マイクのstreamをMediaStreamNodeに入力
        const audioSource = audioContext.createMediaStreamSource(stream);
        audioSource.connect(mediaStreamDestination);

        // 接続先のstreamをMediaStreamに入力
        for(let stream of remoteAudioStream){
            try{
                audioContext.createMediaStreamSource(stream).connect(mediaStreamDestination);
            } catch(e){
                console.log(e);
            }
        }

        // マイクと接続先を合成したMediaStreamを取得
        const composedMediaStream = mediaStreamDestination.stream;
        // マイクと接続先を合成したMediaStreamSourceNodeを取得
        const composedAudioSource = audioContext.createMediaStreamSource(composedMediaStream);

        // 音声のサンプリングをするNodeを作成
        const audioProcessor = audioContext.createScriptProcessor(1024, 1, 1);
        // マイクと接続先を合成した音声をサンプリング
        composedAudioSource.connect(audioProcessor);

        audioProcessor.addEventListener('audioprocess', event => {
            audioData.push(event.inputBuffer.getChannelData(0).slice());
        });

        audioProcessor.connect(audioContext.destination);
    }

録音した音声をバイナリデータ化

hogehoge.js
//音声をエクスポートした後のwavデータ格納用配列
    const waveArrayBuffer = [];
    //仕様の関係で、大きなデータを分けたうちの1つのデータ容量が25MB以下になるよう制御
    if (audioData.length > 250){
        const num = audioData.length/250;
        const count = Math.round(num);

        for (let i=0; i < count; i++){
            const sliceAudioData = audioData.slice(0,249);
            audioData.pop(0,249);
            const waveData = exportWave(sliceAudioData);
            waveArrayBuffer.push(waveData);
        }   
    }else{
        waveArrayBuffer.push(exportWave(audioData));
    }
   //PHPへPOST
    var oReq = new XMLHttpRequest();
    oReq.open("POST", '任意のパス', true);
    oReq.onload = function (oEvent) {
    // Uploaded.
    };

    //複数のデータをblob化するための配列
    const blob = [];
    //waveArrayBufferに入っている複数のデータを1つずつ配列に格納
    waveArrayBuffer.forEach(function(waveBuffer){
        blob.push(new Blob([waveBuffer], {type:'audio/wav'}));
    })

    var fd = new FormData();
    for (let i=0; i < blob.length; i++){
        fd.append('blob'+i,blob[i]);
    }
    // oReq.setRequestHeader('Content-Type','multipart/form-data; name="blob" boundary=\r\n');
    //配列ごとリクエスト送信
    oReq.send(fd);

    function exportWave(audioData) {
    // Float32Arrayの配列になっているので平坦化
    const audioWaveData = flattenFloat32Array(audioData);
    // WAVEファイルのバイナリ作成用のArrayBufferを用意
    const buffer = new ArrayBuffer(44 + audioWaveData.length * 2);

    // ヘッダと波形データを書き込みWAVEフォーマットのバイナリを作成
    const dataView = writeWavHeaderAndData(new DataView(buffer), audioWaveData, sampleRate);

    return buffer;
    }

    // Float32Arrayを平坦化する
    function flattenFloat32Array(matrix) {
        const arraySize = matrix.reduce((acc, arr) => acc + arr.length, 0);
        let resultArray = new Float32Array(arraySize);
        let count = 0;
        for(let i = 0; i < matrix.length; i++) {
            for(let j = 0; j < matrix[i].length; j++) {
            resultArray[count] = audioData[i][j];
            count++;
            }
        }
        return resultArray;
    }
    // ArrayBufferにstringをoffsetの位置から書き込む
    function writeStringForArrayBuffer(view, offset, str) {
        for(let i = 0; i < str.length; i++) {
            view.setUint8(offset + i, str.charCodeAt(i));
        }
    }

    // 波形データをDataViewを通して書き込む
    function floatTo16BitPCM(view, offset, audioWaveData) {
        for (let i = 0; i < audioWaveData.length; i++ , offset += 2) {
            let s = Math.max(-1, Math.min(1, audioWaveData[i]));
            view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
        }
    }

    // モノラルのWAVEヘッダを書き込む
    function writeWavHeaderAndData(view, audioWaveData, samplingRate) {
        // WAVEのヘッダを書き込み(詳しくはWAVEファイルのデータ構造を参照)
        writeStringForArrayBuffer(view, 0, 'RIFF'); // RIFF識別子
        view.setUint32(4, 36 + audioWaveData.length * 2, true); // チャンクサイズ(これ以降のファイルサイズ)
        writeStringForArrayBuffer(view, 8, 'WAVE'); // フォーマット
        writeStringForArrayBuffer(view, 12, 'fmt '); // fmt識別子
        view.setUint32(16, 16, true); // fmtチャンクのバイト数(第三引数trueはリトルエンディアン)
        view.setUint16(20, 1, true); // 音声フォーマット。1はリニアPCM
        view.setUint16(22, 1, true); // チャンネル数。1はモノラル。
        view.setUint32(24, samplingRate, true); // サンプリングレート
        view.setUint32(28, samplingRate * 2, true); // 1秒あたりのバイト数平均(サンプリングレート * ブロックサイズ)
        view.setUint16(32, 2, true); // ブロックサイズ。チャンネル数 * 1サンプルあたりのビット数 / 8で求める。モノラル16bitなら2。
        view.setUint16(34, 16, true); // 1サンプルに必要なビット数。16bitなら16。
        writeStringForArrayBuffer(view, 36, 'data'); // サブチャンク識別子
        view.setUint32(40, audioWaveData.length * 2, true); // 波形データのバイト数(波形データ1点につき16bitなのでデータの数 * 2となっている)

        // WAVEのデータを書き込み
        floatTo16BitPCM(view, 44, audioWaveData); // 波形データ

        return view;
    }

リクエスト受け取り部分(超絶一部抜粋)

hogehoge.php
//リクエスト受け取り
$req = $_FILES
var_dump($req);

//出力結果
array(2) {
  ["blob0"]=>
  array(5) {
    ["name"]=>
    string(4) "blob"
    ["type"]=>
    string(9) "audio/wav"
    ["tmp_name"]=>
    string(14) "/tmp/ランダム文字列"
    ["error"]=>
    int(0)
    ["size"]=>
    int(509996)
  }

おわりに

ご指摘等ありましたら宜しくお願い致します!

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

fputcsvのダブルクォーテーション問題をスッキリ解決する

経緯

文字列をダブルクォーテーションできっちりくくった状態でcsvに吐き出す必要があった。
SQL側で文字列結合とかquote_identとかでダブルクォーテーション付けて結果を取得し、fputcsvで書き出すと下記状態になってしまっていた。

( ゚д゚) ・・・ (つд⊂)ゴシゴシ (;゚д゚)

"""文字列""","""文字列""",....

どうやらfputcsvが残念仕様でPHP的には良くある話のようだ。
割とfputcsvを再発明して関数とかで何とかしてる人が多い気がする。

ワイはそんなのやりたく無い

どうやったか

文字列にして乗り切った

fputcsv($file, 文字列);

file_put_contents($file,implode(",",文字列).PHP_EOL,FILE_APPEND);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】24時間以内に作成されたユーザーの数を集計する【指定範囲のデータ取得】

1日

use DB;

DB::table('users')
  ->orderBy('created_at','desc')
  ->whereRaw('created_at > NOW() - INTERVAL 1 DAY')
  ->count()

30分

  ->whereRaw('created_at > NOW() - INTERVAL 30 MIN')

リファレンス

この記事が作成されている時点では、以下のリファレンスが用意されていたのですが、バージョンアップで参照できなくなるとおもうので、適宜環境にあったリファレンスを参照してください。

image.png

筆者のサーバー環境はMySQL 5.5だったのだけど、実行環境依存だとおもうので、MINとかが使えないバージョンなどもあったりするのだろうか。。。 :thinking:

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

LaravelでAWS認証エラーが起きる?恐らくそれはconfig_cacheの影響です

約6年ぶりくらいにPHP(初Laravel)を触ってるのですがプチハマりしました。残存cacheの影響でハマるとか、物凄く良くありそうなシチュエーションなのに具体的な解決策を見つけられなかったので書いておきます。

LaravelでAWSソリューションを使ってるとこんなエラーレスポンスにめぐりあうかもしれません。

{
  "code": "400",
  "message": "Client error: `GET http://169.254.xxx.xxx/latest/meta-data/iam/security-credentials/` resulted in a `404 Not Found` response:\n<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n\t\"http://www. (truncated...)\n",
  "exception": "GuzzleHttp\\Exception\\ClientException"
}

このエラーはAWSの認証情報であるCredentialsが何かダメですよ!という内容です。
いやいや.envにちゃんとAWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEY書いてあるYO!という時には、恐らくconfigファイルがキャッシュされているせいで正常にAWSのCredentialsが読み込めておらずエラーレスポンスになってる事があります。
ちなみに稼働環境で php artisan config:clear を叩けば最新の.envを読み込み(キャッシュもします)するので解消すると思います。

ただし私の環境ではデプロイにDeployerを使っています。なのでデプロイ時に自動実行してもらう為にtaskへ入れておこうと思ったのですが、どうやら標準のタスクには無いようです。なので自分でdeploy.phpにカスタマイズタスクを記載して呼び出します。

task('artisan:config:clear', function () {
    run('php {{release_path}}/artisan config:clear');
});
/**
 * Main task
 */

desc('Deploy your project');
task('deploy:laravel', [
    'deploy:info',
    'deploy:prepare',
    'deploy:lock',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    'deploy:vendors',
    'deploy:writable',
    'artisan:storage:link',
    'artisan:view:clear',
    'artisan:cache:clear',
    'artisan:config:cache',
    'artisan:optimize',
    // optimizeで何かやってるらしく、ここに書かないとうまく機能しません(面倒なのでちゃんと調べてない)
    'artisan:config:clear',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
]);

ぶっちゃけconfig:clearタスクは省いても良い気がするのですがオーバーヘッドも対してなさそうなので放置しています。

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

文字列・配列の変換など

文字列を配列に変換 

qiita.php
$menu = 'プリン,ショートケーキ,ガトーショコラ,いちごパフェ,パンケーキ';
    $menu = explode(",", $menu);
// , を配列の要素の区切りとする。
    print_r($menu);
//Array ( [0] => プリン [1] => ショートケーキ [2] => ガトーショコラ [3] => いちごパフェ [4] => パンケーキ )

配列を文字列に変換

qiita.php
$menu = implode($menu);
echo $menu;
//プリンショートケーキガトーショコラいちごパフェパンケーキ

//引数に区切りを指定できる
$menu = implode(",", $menu);
echo $menu;
//プリン,ショートケーキ,ガトーショコラ,いちごパフェ,パンケーキ

文字列に区切り(、 とか)がないとき

qiita.php
$num = "01234567890";
    $num = str_split($num);
    print_r($num);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 [10] => 0 )

//第2引数に文字の長さを指定
$num = "01234567890";
    $num = str_split($num, 3);
    print_r($num);
//Array ( [0] => 012 [1] => 345 [2] => 678 [3] => 90 )

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

PHP 変数の誤用

PHPを勉強中です。
サンプルプログラムを作成していて、思ったように動作しないことがあります。
いろいろ、さんざん調べた挙句、変数名が違っていることが往々にしてあります。
たとえば、$_SESSIONを$SESSIONと記述していたりなどです。
PHPのビルトインサーバーやデベロッパーツールにエラーが表示されれば、助かるのですが、そのような方法は無いのでしょうか?

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

【SEO】レガシーなサイトの表示速度を16倍早くした話

前置き

運営しているサービスがかなりレガシーなので、負の遺産が多いです。

入社した頃からサイトの速度が遅いなと感じていながら、日々の業務を追われているうちに後回しになっていました。

それから半年たったある日、SEOに注力する方針となり上司からサービスのサイトの表示速度が遅いので、本日中に何とかしてほしいとのことでようやく時間を割いて対応することになりました。

環境

  • PHP 5.2
  • Breeze framework
  • MariaDB 5.5.62

速度改善で要因特定にやったこと

  • Chromeのデバックツールで表示速度を確認
  • バックエンドかフロントエンドか要因を切り分け
    • バックエンドに時間がかかっていることが判明
  • PHPのソースコード内に速度を図るコードを埋め込み
    • DBの処理に時間がかかっていることが判明

速度改善にやったことまとめ

  1. (効果:◎)DB:EXPLAINで実行計画を行い、INDEXを設定
  2. (効果:△)PHP:ロジックを修正(不要な処理を削除)
  3. (効果:◎)DB:EXPLAINで実行計画を行い、INDEXを設定
  4. (効果:△)PHP:ロジックを修正(取得系の処理改善)
  5. (効果:◎)DB:クエリキャッシュを有効にする

DBの速度改善が効果絶大だったので、対応したことを後記していきます。

DB:EXPLAINで実行計画を行い、INDEXを設定

FYI. MySQLのIndexをはるコツ | Qiita

-- インデックス削除
ALTER TABLE example_table_1 DROP INDEX user_id(user_id);

-- インデックス追加
ALTER TABLE example_table_2 ADD INDEX index_user_id(user_id);

DB:クエリキャッシュを有効にする

DBを再起動せずに反映する方法

SET GLOBAL query_cache_size=64*1024*1024;
SET GLOBAL query_cache_limit=8*1024*1024;
SET GLOBAL query_cache_type=1;
FLUSH QUERY CACHE;

実施結果

サイトの表示速度がなんと「5800ms」から「350ms」になりました。
およそ16倍の速度改善をすることが出来ました。

……クエリキャッシュの威力半端ない!!

スクリーンショット 2018-12-21 16.42.27 (1).png

最後に

ちょっとした設定一つでこんなにもサイトの表示速度が変わるのは感動しました。
むしろ今まで放置だったのも良くないですが…(苦笑)
いま、動いているリソースで改善できることはまだまだありますね!

とはいえ、ミドルウェアのバージョンがそもそも古かったり、フレームワークが古かったり、サーバーのスペックが適切でなかったり……
根本的な要因もあるので、引き続き地道に改善していきつつ思い切った決断をする必要もありそうですね。

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

PHPの終了タグは「?>」を付けるか付けないかは判断しようねという話

◼️理由は終了タグを省略することでファイルの最後にある余空白文字が出力に影響することを防ぐ
◼️htmlでは改行コードを半角スペースとして認識する
◼️htmlテンプレートなどに記述する際は週s量タグは必要

参考:
https://thesaibase.com/php/end-tag

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