<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Recca Tsai on Medium]]></title>
        <description><![CDATA[Stories by Recca Tsai on Medium]]></description>
        <link>https://medium.com/@reccatsai?source=rss-cbb660de6c5e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*FsJmkhVvOStWC57j.</url>
            <title>Stories by Recca Tsai on Medium</title>
            <link>https://medium.com/@reccatsai?source=rss-cbb660de6c5e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 24 May 2026 02:25:00 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@reccatsai/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[PHP 匯入 CSV 時遇到指數怎麼辦]]></title>
            <link>https://medium.com/@reccatsai/php-%E5%8C%AF%E5%85%A5-csv-%E6%99%82%E9%81%87%E5%88%B0%E6%8C%87%E6%95%B8%E6%80%8E%E9%BA%BC%E8%BE%A6-46befcd02ad3?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/46befcd02ad3</guid>
            <category><![CDATA[php]]></category>
            <category><![CDATA[float]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Tue, 16 Jul 2024 06:05:35 GMT</pubDate>
            <atom:updated>2024-07-16T06:34:38.060Z</atom:updated>
            <content:encoded><![CDATA[<p>從 Excel 將資料轉存時，只要是數字欄位就很有可能被轉為指數的型式，例如</p><pre>0.000089 =&gt; 8.90E-05</pre><p>如果單純把 8.90E-05 這個指數型式的數字直接塞入 MySQL，MySQL 會自動轉換，是完全沒問題的，但如果需要透過 bcmul 這一類的函數進行運算時，PHP 就會丟出 `bcmul(): bcmath function argument is not well-formed` 這個訊息。所以我們就必須把指數轉回 float 的型式，只要執行以下程式即可</p><pre>echo sprintf(&#39;%f&#39;, &#39;8.90E-05&#39;); // 輸出 0.000089</pre><p>但如果是 8.90E-15 這個數字會出現什麼結果呢？</p><pre>echo sprintf(&#39;%f&#39;, &#39;8.90E-12&#39;); // 輸出 0.000000 </pre><p>小數點位數大於 5 轉換是會有問題的，所以我們將程式進化一下</p><pre>function toFloat($number) {<br> if (preg_match(&#39;/E-(\d+)/&#39;, (string) $number, $matched)) {<br>  $length = max(5, ((int) $matched[1]) + 1);<br>  <br>  return sprintf(&#39;%.&#39;.$length.&#39;f&#39;, $number);<br> }<br> <br> return sprintf(&#39;%f&#39;, $number);<br>}<br><br>echo toFloat(&#39;8.90E-12&#39;); // 輸出 0.0000000000089</pre><p>這樣就能很好的解決小數點位數過多的問題了</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=46befcd02ad3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Laravel Blade Template 遇到重覆區塊的解決方]]></title>
            <link>https://medium.com/@reccatsai/laravel-blade-template-%E9%81%87%E5%88%B0%E9%87%8D%E8%A6%86%E5%8D%80%E5%A1%8A%E7%9A%84%E8%A7%A3%E6%B1%BA%E6%96%B9-32cf068edb0b?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/32cf068edb0b</guid>
            <category><![CDATA[repeat]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[blade]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Tue, 07 May 2024 07:47:27 GMT</pubDate>
            <atom:updated>2024-05-07T07:47:27.254Z</atom:updated>
            <content:encoded><![CDATA[<pre>&lt;div&gt;<br>  Hello World<br>&lt;/div&gt;<br><br>this is content<br><br>&lt;div&gt;<br>  Hello World<br>&lt;/div&gt;</pre><p>以上的 HTML 發現有兩個 Hello World 的重覆 HTML，這時我們想到把清除掉的方法有</p><ol><li>使用 @include directive</li></ol><p>建立 hello-world.blade.php`</p><pre>&lt;div&gt;<br>  Hello World<br>&lt;/div&gt;</pre><p>使用 @include directive 載入</p><pre>@include(&#39;hello-world&#39;)<br><br>this is content<br><br>@include(&#39;hello-world&#39;)</pre><p>2. 使用 Component</p><p>建立 `components/hello-world.blade.php`</p><pre>&lt;div&gt;<br>  Hello World<br>&lt;/div&gt;</pre><p>使用 &lt;x-hello-world /&gt;載入</p><pre>&lt;x-hello-world /&gt;<br><br>this is content<br><br>&lt;x-hello-world /&gt;</pre><p>3. 使用 ob_start</p><pre>@php(ob_start())<br>&lt;div&gt;<br> Hello World<br>&lt;/div&gt;<br>@php($hello = ob_get_clean())<br><br>{!! $hello !!}<br><br>this is content<br><br>{!! $hello !!}</pre><p>這三個方案都可以消除重覆的區塊，前二個方案是 Laravel 官方內建的方案，但必須將重覆的區塊移至新的檔案才能再載入，而第三個方案則是利用 ob_start 將輸出存入 buffer 後再取出，就不必再新增檔案了</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=32cf068edb0b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Alpine Validate Errors For Laravel]]></title>
            <link>https://medium.com/@reccatsai/alpine-validate-errors-for-laravel-e210d9b6b5e2?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/e210d9b6b5e2</guid>
            <category><![CDATA[validate]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[error]]></category>
            <category><![CDATA[alpine]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Fri, 19 Apr 2024 09:30:26 GMT</pubDate>
            <atom:updated>2024-04-29T03:13:09.512Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dASzVfUDrA0ONHXftnrhWg.jpeg" /></figure><p>Laravel Validate 有異常時，返回上一頁我們就可以在 blade 裡這樣寫</p><pre>&lt;label for=&quot;email&quot;&gt;Email&lt;/label&gt;<br> <br>&lt;input id=&quot;title&quot;<br>    type=&quot;email&quot;<br>    name=&quot;email&quot;<br>    class=&quot;@error(&#39;email&#39;) is-invalid @enderror&quot;&gt;<br> <br>@error(&#39;email&#39;)<br>    &lt;div class=&quot;alert alert-danger&quot;&gt;{{ $message }}&lt;/div&gt;<br>@enderror</pre><p>但在 Laravel Validate 出錯且 request 為 ajax 則會回應以下的資訊</p><pre>{<br>  data: {<br>      errors: {<br>          email: [&#39;The email field must be a valid email address.&#39;],<br>          name: [&#39;The name field is required.&#39;],<br>      },<br>      message: &#39;The name field is required. (and 1 more error)&#39;,<br>  }<br>}</pre><p>因為不想再另外記其他的使用方式，所以就直接幫 Alpine 寫了一個 Plugin</p><pre>// errors.js<br>class MessageBag {<br>    constructor(errors = {}) {<br>        this.errors = errors;<br>    }<br><br>    set(key, value) {<br>        return this.put(key, value);<br>    }<br><br>    put(key, value) {<br>        const values = typeof key === &#39;object&#39; ? key : {[key]: value};<br><br>        for (const x in values) {<br>            let val = values[x];<br><br>            this.errors[x] = typeof val === &#39;string&#39; ? [val] : val;<br>        }<br><br>        return this;<br>    }<br><br>    get(key) {<br>        if (!this.errors.hasOwnProperty(key) || !this.errors[key]) {<br>            this.put(key, null);<br>        }<br><br>        return this.errors[key];<br>    }<br><br>    has(key) {<br>        return this.get(key) !== null;<br>    }<br><br>    first(key) {<br>        if (!this.has(key)) {<br>            return null;<br>        }<br><br>        const value = this.get(key);<br><br>        return value instanceof Array ? value[0] : value;<br>    }<br><br>    remove(...keys) {<br>        keys.forEach(key =&gt; this.put(key, null));<br>    }<br><br>    clear() {<br>        this.remove(...Object.keys(this.errors));<br>    }<br><br>    all() {<br>        return Object.keys(this.errors).reduce((acc, key) =&gt; {<br>            const value = this.get(key);<br><br>            return value === null ? acc : {...acc, [key]: value};<br>        }, {});<br>    }<br><br>    registerAxiosInterceptor(axios) {<br>        const beforeRequest = (config) =&gt; {<br>            this.clear();<br><br>            return config;<br>        };<br>        const onError = (err) =&gt; {<br>            const {status, data} = err.response;<br><br>            if (status === 422) {<br>                this.set(data.errors);<br>            }<br><br>            return Promise.reject(err);<br>        };<br><br>        axios.interceptors.request.use(beforeRequest, err =&gt; Promise.reject(err));<br>        axios.interceptors.response.use(response =&gt; response, onError);<br>    }<br>}<br><br>export default function (Alpine) {<br>    const errors = Alpine.reactive(new MessageBag({}));<br>    Alpine.magic(&#39;errors&#39;, () =&gt; errors);<br>    Object.defineProperty(Alpine, &#39;errors&#39;, {get: () =&gt; errors});<br>}</pre><p>初始化程式</p><pre>// bootstrap.js<br>import Alpine from &#39;alpinejs&#39;;<br>import Axios from &#39;axios&#39;;<br>import errors from &#39;./errors&#39;;<br><br>errors(Alpine);<br>Alpine.start();<br><br>window.axios = Axios.create();<br>window.axios.defaults.headers.common[&#39;X-Requested-With&#39;] = &#39;XMLHttpRequest&#39;;<br>Alpine.errors.registerAxiosInterceptor(window.axios);</pre><p>把以上的程式複製到 js 資料夾後 build 完，就可以開心的使用了</p><p>再附上測試程式(包含實際使用方式)</p><pre>// errors.test.js<br>import { fireEvent, screen } from &#39;@testing-library/dom&#39;;<br>import axios from &#39;axios&#39;;<br>import MockAdapter from &#39;axios-mock-adapter&#39;;<br>import Alpine from &#39;alpinejs&#39;;<br>import plugin from &#39;./errors&#39;;<br><br>describe(&#39;Alpine $errors&#39;, () =&gt; {<br>    const givenComponent = (name) =&gt; {<br>        const component = document.createElement(&#39;div&#39;);<br>        component.innerHTML = `<br>            &lt;div x-data&gt;<br>                &lt;div&gt;<br>                    &lt;label for=&quot;${name}&quot; class=&quot;block text-sm font-medium leading-6 text-gray-900&quot;&gt;${name}&lt;/label&gt;<br>                    &lt;div class=&quot;relative mt-2 rounded-md shadow-sm&quot;&gt;<br>                        &lt;input type=&quot;text&quot; name=&quot;${name}&quot; id=&quot;${name}&quot;<br>                               class=&quot;block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6&quot;<br>                               :class=&quot;{&#39;text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500&#39;: $errors.has(&#39;${name}&#39;)}&quot;<br>                               @keyup=&quot;$errors.remove(&#39;${name}&#39;)&quot;<br>                               role=&quot;input&quot;&gt;<br>                    &lt;/div&gt;<br><br>                    &lt;template x-if=&quot;$errors.has(&#39;${name}&#39;)&quot;&gt;<br>                        &lt;p x-text=&quot;$errors.first(&#39;${name}&#39;)&quot; class=&quot;mt-2 text-sm text-red-600&quot; id=&quot;${name}-error&quot; role=&quot;error-message&quot;&gt;&lt;/p&gt;<br>                    &lt;/template&gt;<br>                &lt;/div&gt;<br>            &lt;/div&gt;<br>        `;<br>        document.body.append(component);<br><br>        return component;<br>    };<br><br>    beforeAll(() =&gt; {<br>        plugin(Alpine);<br><br>        const mock = new MockAdapter(axios);<br>        mock.onPost(&#39;/users&#39;).reply(422, {<br>            &#39;message&#39;: &#39;The email field must be a valid email address.&#39;,<br>            &#39;errors&#39;: {&#39;email&#39;: [&#39;The email field must be a valid email address.&#39;]},<br>        });<br>        Alpine.$errors.registerAxiosInterceptor(axios);<br><br>        Alpine.start();<br>    });<br><br>    afterAll(() =&gt; Alpine.stopObservingMutations());<br><br>    beforeEach(() =&gt; {<br>        document.body.innerHTML = &#39;&#39;;<br>        givenComponent(&#39;email&#39;);<br>        givenComponent(&#39;password&#39;);<br>    });<br><br>    afterEach(() =&gt; document.body.innerHTML = &#39;&#39;);<br><br>    const getInvalidInputs = () =&gt; screen.queryAllByRole(&#39;input&#39;).filter(el =&gt; el.classList.contains(&#39;text-red-900&#39;));<br>    const getErrorMessages = () =&gt; screen.queryAllByRole(&#39;error-message&#39;);<br><br>    async function expectShowError() {<br><br><br>        try {<br>            await axios.post(&#39;/users&#39;);<br>        } catch (e) {<br>        }<br><br>        expect(getInvalidInputs()).toHaveLength(1);<br>        expect(getErrorMessages()).toHaveLength(1);<br>        expect(getErrorMessages()[0].innerHTML).toContain(&#39;The email field must be a valid email address.&#39;);<br>    }<br><br>    it(&#39;show errors&#39;, async () =&gt; {<br>        await expectShowError();<br>    });<br><br>    it(&#39;clear errors&#39;, async () =&gt; {<br>        await expectShowError();<br><br>        await Alpine.nextTick(() =&gt; Alpine.$errors.clear());<br><br>        expect(getInvalidInputs()).toHaveLength(0);<br>        expect(getErrorMessages()).toHaveLength(0);<br>    });<br><br>    it(&#39;key up clear input error&#39;, async () =&gt; {<br>        await expectShowError();<br><br>        const invalidInput = getInvalidInputs()[0];<br>        await Alpine.nextTick(() =&gt; fireEvent.keyUp(invalidInput));<br><br>        expect(invalidInput.classList).not.toContain(&#39;text-red-900&#39;);<br>    });<br>});</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e210d9b6b5e2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Laravel Vite host 設定]]></title>
            <link>https://medium.com/@reccatsai/laravel-vite-host-%E8%A8%AD%E5%AE%9A-4324d51149d3?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/4324d51149d3</guid>
            <category><![CDATA[valet]]></category>
            <category><![CDATA[host]]></category>
            <category><![CDATA[laravel-vite]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Mon, 20 Mar 2023 07:08:10 GMT</pubDate>
            <atom:updated>2023-03-20T07:08:25.885Z</atom:updated>
            <content:encoded><![CDATA[<p>Laravel Vite 如果使用預設的設定檔</p><pre>import { defineConfig } from &#39;vite&#39;;<br>import laravel from &#39;laravel-vite-plugin&#39;;<br><br>export default defineConfig({<br>    plugins: [<br>        laravel({<br>            input: [&#39;resources/css/app.css&#39;, &#39;resources/js/app.js&#39;],<br>            refresh: true,<br>        }),<br>    ],<br>});</pre><p>當執行 npm run dev 時，@vite 會將 asset 的路徑指向 <a href="http://localhost">http://localhost</a>，這時只需要加入 server.host 的設定即可</p><pre>import { defineConfig } from &#39;vite&#39;;<br>import laravel from &#39;laravel-vite-plugin&#39;;<br><br>const host = &#39;xxx.test&#39;;<br><br>export default defineConfig({<br>    server: {<br>        host: host,<br>        hmr: {<br>            host: host,<br>        },<br>    },<br>    plugins: [<br>        laravel({<br>            input: [&#39;resources/css/app.css&#39;, &#39;resources/js/app.js&#39;],<br>            refresh: true,<br>        }),<br>    ],<br>});</pre><p>如果使用 valet 且想要使用 https 可以至資料夾內執行 valet secure後再使使用以下設定檔即可</p><pre>import { defineConfig } from &#39;vite&#39;;<br>import laravel from &#39;laravel-vite-plugin&#39;;<br><br>const host = &#39;xxx.test&#39;;<br><br>export default defineConfig({<br>    plugins: [<br>        laravel({<br>            input: [&#39;resources/css/app.css&#39;, &#39;resources/js/app.js&#39;],<br>            refresh: true,<br>            valetTls: host,<br>        }),<br>    ],<br>});</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4324d51149d3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PHP 取得 Third Package Version]]></title>
            <link>https://medium.com/@reccatsai/php-%E5%8F%96%E5%BE%97-third-package-version-10fd396b3bb6?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/10fd396b3bb6</guid>
            <category><![CDATA[version]]></category>
            <category><![CDATA[composer]]></category>
            <category><![CDATA[php]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Sat, 25 Feb 2023 10:04:03 GMT</pubDate>
            <atom:updated>2023-02-25T10:04:03.869Z</atom:updated>
            <content:encoded><![CDATA[<p>用 composer 安裝 third package 時，我們可以透過這個方法來取得版本號</p><pre>InstalledVersions::getVersion(&#39;laravel/framework&#39;);</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=10fd396b3bb6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Laravel 10 Database Expression 新功能]]></title>
            <link>https://medium.com/@reccatsai/laravel-10-database-expression-%E6%96%B0%E5%8A%9F%E8%83%BD-df7d36edc060?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/df7d36edc060</guid>
            <category><![CDATA[raw]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[expression]]></category>
            <category><![CDATA[eloquent]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Mon, 20 Feb 2023 05:41:20 GMT</pubDate>
            <atom:updated>2023-02-22T12:53:18.916Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dASzVfUDrA0ONHXftnrhWg.jpeg" /></figure><p>這個功能目前在 Laravel 的官方文件沒提到的，根據 <a href="https://github.com/laravel/framework/pull/44784">PR</a> 的描述，當遇到資料庫提供『不同函式名但功能相同的』的 sql function 我們就可以利用 Database Expression 來幫我們解決這個問題。</p><p>以常用的 MySQL 的 IF為例，我們可能會寫出以下的 code</p><pre>namespace Tests\Feature;<br><br>use Illuminate\Foundation\Testing\RefreshDatabase;<br>use Illuminate\Support\Facades\DB;<br>use Tests\TestCase;<br><br>class ExampleTest extends TestCase<br>{<br>    use RefreshDatabase;<br><br>    public function test_database_expression(): void<br>    {<br>        $result = DB::query()<br>            -&gt;selectRaw(&#39;IF(10 &gt; 1, 1, 0) AS value&#39;)<br>            -&gt;first();<br><br>        self::assertEquals(1, $result-&gt;value);<br>    }<br>}</pre><p>但當我們把資料庫改為 SQLite 時再執行一次測試則會出現 no such function的錯誤訊息</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DWYfi_GZrY2_n9uAbloQww.png" /></figure><p>為了讓測試通則會將程式改為</p><pre>namespace Tests\Feature;<br><br>use Illuminate\Database\MySqlConnection;<br>use Illuminate\Foundation\Testing\RefreshDatabase;<br>use Illuminate\Support\Facades\DB;<br>use Tests\TestCase;<br><br>class ExampleTest extends TestCase<br>{<br>    use RefreshDatabase;<br><br>    public function test_database_expression(): void<br>    {<br>        $isMySQL = is_a(DB::connection(), MySqlConnection::class);<br><br>        $result = DB::query()<br>            -&gt;when($isMySQL, function ($query) {<br>                return $query-&gt;selectRaw(&#39;IF(10 &gt; 1, 1, 0) AS value&#39;);<br>            })<br>            -&gt;when(! $isMySQL, function ($query) {<br>                return $query-&gt;selectRaw(&#39;CASE WHEN 10 &gt; 1 THEN 1 ELSE 0 END AS value&#39;);<br>            })<br>            -&gt;first();<br><br>        self::assertEquals(1, $result-&gt;value);<br>    }<br>}</pre><p>這樣使用的資料庫不論是 MySQL 或 SQLite，程式都可以正常的運作，但 code 看起來就相對複雜多了，所以我們可以利用這個 Expression 再將程式做一次調整，我們最終可以將程式碼改為</p><pre>namespace Tests\Feature;<br><br>use Illuminate\Contracts\Database\Query\Expression;<br>use Illuminate\Database\Grammar;<br>use Illuminate\Database\Query\Grammars\SQLiteGrammar;<br>use Illuminate\Foundation\Testing\RefreshDatabase;<br>use Illuminate\Support\Facades\DB;<br>use Tests\TestCase;<br><br>class ExampleTest extends TestCase<br>{<br>    use RefreshDatabase;<br><br>    public function test_database_expression(): void<br>    {<br>        $result = DB::query()<br>            -&gt;select(new IfExpression(&#39;value&#39;, &#39;10 &gt; 1&#39;, 1, 0))<br>            -&gt;first();<br><br>        self::assertEquals(1, $result-&gt;value);<br>    }<br>}<br><br>class IfExpression implements Expression<br>{<br>    public function __construct(<br>        private readonly string $alias,<br>        private readonly string $condition,<br>        private readonly mixed $true,<br>        private readonly mixed $false<br>    ) {<br>    }<br><br>    public function getValue(Grammar $grammar)<br>    {<br>        return match (get_class($grammar)) {<br>            SQLiteGrammar::class =&gt; &quot;CASE WHEN $this-&gt;condition THEN $this-&gt;true ELSE $this-&gt;false END AS $this-&gt;alias&quot;,<br>            default =&gt; &quot;IF($this-&gt;condition, $this-&gt;true, $this-&gt;false) AS $this-&gt;alias&quot;,<br>        };<br>    }<br>}</pre><p>這樣的調整，不但讓我們程式碼變的容易閱讀之外，也幫助我們更容易的消除重覆的程式碼，已經升級至 Laravel 10 的人可以多加利用</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=df7d36edc060" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Laravel 為SQLite 增加自定 Function]]></title>
            <link>https://medium.com/@reccatsai/laravel-%E7%82%BAsqlite-%E5%A2%9E%E5%8A%A0%E8%87%AA%E5%AE%9A-function-5ca116423f32?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/5ca116423f32</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[phpunit]]></category>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[sqlite]]></category>
            <category><![CDATA[custom-functions]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Fri, 17 Feb 2023 03:27:51 GMT</pubDate>
            <atom:updated>2023-02-17T03:27:51.877Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SXW0X4jq_YEhhvD6OO_KMA.png" /></figure><p>每個資料庫的內建的 function 其實都不一樣的，當把 SQLite 當成主要測試資料庫難免會遇到 SQLite 不支援的 function，但 PHP 是支援 SQLite 的自定義 function 的，一樣直接來看實例</p><p>我們先在在 routes/web.php 裡寫以下的測試程式</p><pre>Route::get(&#39;/&#39;, function() {<br>  return User::query()-&gt;orderByRaw(&#39;FIELD(id, 3, 5, 4, 1, 2)&#39;)-&gt;get();<br>});</pre><p>接下來我們可以加入以下的測試案例</p><pre>namespace Tests\Feature;<br><br>use App\Models\User;<br>use Illuminate\Foundation\Testing\RefreshDatabase;<br>use Tests\TestCase;<br><br>class ExampleTest extends TestCase<br>{<br>    use RefreshDatabase;<br><br>    public function test_sql_function(): void<br>    {<br>        User::factory()-&gt;count(5)-&gt;create();<br><br>        $data = $this-&gt;get(&#39;/&#39;)-&gt;assertStatus(200)-&gt;collect();<br><br>        // 預期得到 id 排序為 3, 5, 4, 1, 2 的結果<br>        self::assertEquals([3, 5, 4, 1, 2], $data-&gt;pluck(&#39;id&#39;)-&gt;toArray());<br>    }<br>}</pre><p>執行測試後我們會得到以下的結果</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lIKHZLq6O1BJB6ZOTbUeIQ.png" /></figure><p>接著我們修改 phpunit.xml 把資料庫改為 SQLite 再執行一次測試會得到 no such function FIELD的錯誤訊息，因為 SQLite 並不支援 FIELD 的 function</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KdLTmE_KAH-rV_HJFpve8Q.png" /></figure><p>為了讓 SQLite 支援 FILED 的 function，我們可以在 TestCase 裡加入以下程式碼</p><pre>namespace Tests;<br><br>use Illuminate\Database\SQLiteConnection;<br>use Illuminate\Foundation\Testing\TestCase as BaseTestCase;<br>use Illuminate\Support\Facades\DB;<br><br>abstract class TestCase extends BaseTestCase<br>{<br>    use CreatesApplication;<br><br>    protected function setUp(): void<br>    {<br>        parent::setUp();<br>        $connection = DB::connection();<br>        // 判斷是否為 SQLite<br>        if (is_a($connection, SQLiteConnection::class)) {<br>            // 自訂 SQLite function<br>            $connection-&gt;getPdo()-&gt;sqliteCreateFunction(<br>                &#39;FIELD&#39;,<br>                static fn($id, ...$array) =&gt; array_search($id, $array)<br>            );<br>        }<br>    }<br>}</pre><p>加完以上程式碼後我們就可以正常的執行測試了</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5ca116423f32" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[League\Flysystem\UnableToCheckFileExistence]]></title>
            <link>https://medium.com/@reccatsai/league-flysystem-unabletocheckfileexistence-6a3ffc3de2f2?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/6a3ffc3de2f2</guid>
            <category><![CDATA[s3]]></category>
            <category><![CDATA[flysystem]]></category>
            <category><![CDATA[laravel]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Thu, 19 Jan 2023 03:12:27 GMT</pubDate>
            <atom:updated>2023-01-19T03:12:27.840Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SXW0X4jq_YEhhvD6OO_KMA.png" /></figure><p>執行以下的程式</p><pre>Storage::disk(&#39;s3&#39;)-&gt;exists(&#39;path/to.jpg&#39;);</pre><p>檔案不存在時拋出 League\Flysystem\UnableToCheckFileExistence，而完整的錯誤訊息則是</p><pre>Client error: `HEAD https://xxx.amazonaws.com/path/to.jpg` resulted in a `403 Forbidden` response</pre><p>竟然會是 403 Forbidden？但如果是檔案存在的情況則會正常回應 true，所以不會是 token 錯誤，當下就認定為是 thrid-party package 有 bug，但實際查了一下 issue，查到了<a href="https://github.com/laravel/framework/issues/45639">這一篇討論</a>，它的結論是</p><blockquote>We are unfortunately left in a sad state because the culprit is the AWS underlying API. It throws an ambiguous 403 Forbidden when user has s3:getObject but does not have s3:ListBucket. In this case, the 403 is always because the file does not exist, but we have no way to determine whether this 403 is caused by file not exist or if the user has no permission at all.</blockquote><p>所以需要再給予 <em>s3:ListBucket</em>這個權限，檔案不存在就會正常回應 false，如果沒有 <em>s3:ListBucket</em>這個權限，則會回應 403</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6a3ffc3de2f2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Waiting for thread pool to idle before forking]]></title>
            <link>https://medium.com/@reccatsai/waiting-for-thread-pool-to-idle-before-forking-b4631027bb23?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/b4631027bb23</guid>
            <category><![CDATA[grpc]]></category>
            <category><![CDATA[php]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Wed, 18 Jan 2023 15:37:51 GMT</pubDate>
            <atom:updated>2023-01-18T15:37:51.713Z</atom:updated>
            <content:encoded><![CDATA[<pre>E0118 22:57:35.141869000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking<br>E0118 22:57:38.143735000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking<br>E0118 22:57:41.145384000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking<br>E0118 22:57:44.149256000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking<br>E0118 22:57:47.150471000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking<br>E0118 22:57:50.154267000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking<br>E0118 22:57:53.157349000 140704357886144 thread_pool.cc:253]           Waiting for thread pool to idle before forking</pre><p>PHPUnit 跑測試的時候一直出現這訊息，這時只需要執行</p><pre>pecl install -o -f grpc-1.49.0</pre><p>把 grpc 降到 1.49.0 就不會出現這個訊息了</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b4631027bb23" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[[Laravel] Container 和 Facade 的關聯]]></title>
            <link>https://medium.com/@reccatsai/laravel-container-%E5%92%8C-facade-%E7%9A%84%E9%97%9C%E8%81%AF-29529c01aeb0?source=rss-cbb660de6c5e------2</link>
            <guid isPermaLink="false">https://medium.com/p/29529c01aeb0</guid>
            <category><![CDATA[laravel]]></category>
            <category><![CDATA[facade]]></category>
            <category><![CDATA[containers]]></category>
            <dc:creator><![CDATA[Recca Tsai]]></dc:creator>
            <pubDate>Thu, 12 Jan 2023 05:34:12 GMT</pubDate>
            <atom:updated>2023-01-12T05:40:11.844Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SXW0X4jq_YEhhvD6OO_KMA.png" /></figure><p>延續 <a href="https://medium.com/@reccatsai/how-to-use-laravel-container-to-register-third-party-package-ddfe00f31e2f">Laravel Container</a> 這一篇，我們今天就來看看 Container 和 Facades 的關聯是什麼！</p><p>首先我先把程式做個調整</p><pre>// FakeApi 不變<br>namespace App;<br><br>class FakeApi<br>{<br>    private string $token;<br><br>    public function __construct(string $token)<br>    {<br>        $this-&gt;token = $token;<br>    }<br><br>    public function getToken(): string<br>    {<br>        return $this-&gt;token;<br>    }<br>}<br><br> </pre><pre>namespace App\Providers;<br><br>use App\FakeApi;<br>use Illuminate\Support\ServiceProvider;<br>use Illuminate\Support\Str;<br><br>class AppServiceProvider extends ServiceProvider<br>{<br>    /**<br>     * Register any application services.<br>     *<br>     * @return void<br>     */<br>    public function register()<br>    {<br>        // 將 API 的 token 改為亂數<br>        $this-&gt;app-&gt;bind(FakeApi::class, fn() =&gt; new FakeApi(Str::random()));<br>    }<br><br>    /**<br>     * Bootstrap any application services.<br>     *<br>     * @return void<br>     */<br>    public function boot()<br>    {<br>        //<br>    }<br>}</pre><pre>namespace Tests\Feature;<br><br>use App\FakeApi;<br>use Tests\TestCase;<br><br>class FacadeTest extends TestCase<br>{<br>    public function test_facade(): void<br>    {<br>        /** @var FakeApi $fakeApi */<br>        $fakeApi = app(FakeApi::class);<br>        /** @var FakeApi $fakeApi2 */<br>        $fakeApi2 = app(FakeApi::class);<br>      <br>        self::assertEquals($fakeApi-&gt;getToken(), $fakeApi2-&gt;getToken());<br>    }<br>}</pre><p>測試就改為從 container 取出兩次 FakeApi，接著試試看執行測試會得到什麼結果 …</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HHdHCqPkH6XAyqjIP2keUg.png" /></figure><p>果然出現錯誤，因為我們在 Container 裡註冊 FakeApi 是使用 bind，所以每次從 Container 取出 FakeApi 就會執行 new FakeApi，那我們應該怎麼修改呢？</p><pre>namespace App\Providers;<br><br>use App\FakeApi;<br>use Illuminate\Support\ServiceProvider;<br>use Illuminate\Support\Str;<br><br>class AppServiceProvider extends ServiceProvider<br>{<br>    /**<br>     * Register any application services.<br>     *<br>     * @return void<br>     */<br>    public function register()<br>    {<br>        // bind 改為 singleton<br>        $this-&gt;app-&gt;singleton(FakeApi::class, fn() =&gt; new FakeApi(Str::random()));<br>    }<br><br>    /**<br>     * Bootstrap any application services.<br>     *<br>     * @return void<br>     */<br>    public function boot()<br>    {<br>        //<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9iOCdI4DAt2XFaA7zqOPDQ.png" /></figure><p>只需要把 bind 改為 singleton，每次從 Container 取出 FakeApi 就會是同一個 instance 了</p><p>接下來建立一個 FakeApi 的 Facade</p><pre>namespace App\Facades;<br><br>use Illuminate\Support\Facades\Facade;<br><br>class FakeApi extends Facade<br>{<br>    protected static function getFacadeAccessor()<br>    {<br>        // 指定 Container 註冊的名稱<br>        return \App\FakeApi::class;<br>    }<br>}</pre><p>建立完 Facade 之後再將測試做個調整</p><pre>namespace Tests\Feature;<br><br>use App\Facades\FakeApi as FakeApiFacade;<br>use App\FakeApi;<br>use Tests\TestCase;<br><br>class FacadeTest extends TestCase<br>{<br>    public function test_facade(): void<br>    {<br>        /** @var FakeApi $fakeApi */<br>        $fakeApi = app(FakeApi::class);<br><br>        self::assertEquals($fakeApi-&gt;getToken(), FakeApiFacade::getToken());<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9iOCdI4DAt2XFaA7zqOPDQ.png" /></figure><p>測試一樣會通過，所以由這個例子就可以證明，Facade 是透過 getFacadeAccessor 這個 method 去 Container 裡取得 instance，再透過 Facade 的 static method 去執行 instance 的 method</p><p>接下來可能會有人問 Laravel 的 DB Facade 我沒看到 class 名啊？</p><pre>namespace Illuminate\Support\Facades;<br><br>class DB extends Facade<br>{<br>    /**<br>     * Get the registered name of the component.<br>     *<br>     * @return string<br>     */<br>    protected static function getFacadeAccessor()<br>    {<br>        return &#39;db&#39;;<br>    }<br>}</pre><p>一樣將程式做個調整</p><pre>namespace App\Providers;<br><br>use App\FakeApi;<br>use Illuminate\Support\ServiceProvider;<br>use Illuminate\Support\Str;<br><br>class AppServiceProvider extends ServiceProvider<br>{<br>    /**<br>     * Register any application services.<br>     *<br>     * @return void<br>     */<br>    public function register()<br>    {<br>        $this-&gt;app-&gt;singleton(FakeApi::class, fn() =&gt; new FakeApi(Str::random()));<br>        // 增加別名<br>        $this-&gt;app-&gt;singleton(&#39;fake-api&#39;, fn() =&gt; $this-&gt;app-&gt;make(FakeApi::class));<br>    }<br><br>    /**<br>     * Bootstrap any application services.<br>     *<br>     * @return void<br>     */<br>    public function boot()<br>    {<br>        //<br>    }<br>}</pre><pre>namespace App\Facades;<br><br>use Illuminate\Support\Facades\Facade;<br><br>class FakeApi extends Facade<br>{<br>    protected static function getFacadeAccessor()<br>    {<br>        // 指定 Container 註冊的名稱(別名)<br>        return &#39;fake-api&#39;;<br>    }<br><br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9iOCdI4DAt2XFaA7zqOPDQ.png" /></figure><p>測試一樣會通過，所以透過這個例子我們可以得知，Laravel Container 不僅僅可以註冊 class name 也可以直接註冊字串( FakeApi::class 實際上就是一個字串)，讓我們可以透過『字串』去 Container 裡取得 instance 的</p><p>同場加映，以 DB Facade 為例，在剛接觸 Laravel 真的不太容易找到 ‘db’ 這個字串在哪兒註冊到Container 裡，但我們可以使用 grep 暴力找出來的</p><pre>grep -rnw ./vendor -e &quot;\$this-&gt;app-&gt;\(singleton\|bind\)(&#39;db&#39;&quot;</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=29529c01aeb0" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>