2022年6月12日 星期日

PHP開發筆記 - 書:現代PHP, 新的特點及良好習慣(Modern PHP: New Features and Good Practices)

書:現代PHP, 新的特點及良好習慣(Modern PHP: New Features and Good Practices)

幾週前,找資料時看到了這本書,且幾年前也有人在 IT 鐵人賽 提到。這是一本 2015 年 02 月歐萊禮出的書。只是近幾年已經有許多熱門的語言跟誇架,如 Python, Ruby(RoR), Javascript(Node.js), Golang,似乎誰也沒在記得 PHP、Facebook 出的 HHVM?我也是翻了書才知道還有個 Hack 語言。
沒想到自己已經寫了超過15年PHP了吧?工作上真的還滿夠用,偶爾寫寫 python 跟 php 工具做事。但看了這本 Modern PHP 後,反而補充一段遺失的知識,算是滿值得的。書中的內容對我來說大概有50%以上是已知的,很快就看完了,屬於工作上或學生時代就知道的事,也代表持續使用 PHP 多少都會有所成長,但實在不如讀書吸收的知識效率高,翻出看個小時就就獲得幾年功力。

另外一提,書中有些工具教學的範例套件不見得是現在熱門的工具,建議使用上時還先找一下網路上資源,例如當紅的 PHP Mail 套件就是 PHPMailer ,我也在 2015 年用 PHPMailer 幫公司開發一套可發送百萬 EDM 的小服務,近年則是被同事擴充成可自動化排程架構了。

總之,我還是買了一本來收藏,也詢問 碁峰圖書 得知沒有電子書服務(早年 O'Reilly 有可以下的電子書,並靠 Social DRM,現在則改成訂閱制)。往後則是使用PHP語言時,要時時提醒自己多用 composer 管理做事,包括工具開發等。如果持續朝向如何規範更加的產出程式碼品質時,那真的有源源不絕的目標可以邁進的。

2022年6月10日 星期五

PHP 開發筆記 - 使用 PHP-CS-FIXER 維護團隊的 Coding Style

使用 PHP 語言開發網站服務時,採用各類框架後,其撰寫風格已定下八九成了,那為何還需要呢?主要用來提升產出品質。就像程式碼都能動,QA也驗證過,也上線服務百人千人了,這時有測試涵蓋率資訊,可以讓未來增減功能時,更加大膽。

稍微研究後,在 PHP 語言上,可以直接使用 PHP Coding Standards Fixer 工具,整體上還滿舒適的,裡頭有提到非常多編碼風格檢驗,基本上用預設的即可。備忘一下:

首先故意把 php-cs-fixer 安裝在程式碼上一層名為 tools 目錄,且 tools 內有 composer.phar 工具可用:

% mkdir -p tools/php-cs-fixer
% php tools/composer.phar require --working-dir=tools/php-cs-fixer friendsofphp/php-cs-fixer

先列出當前目錄中,有哪些要修改的項目:

% cd src
% ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff .

對當前目錄進行修正的用法:

% cd src
% ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix .

接著,可以把上面兩個指令包在 composer.json 中,靠 run-script 處理:

% cd src
% cat composer.json | jq '.scripts'
{
  "test": "phpunit",
  "cs-diff": "../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff .",
  "cs": "../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix ."
}

使用方式:

% ../tools/composer.phar run-script cs-diff
> ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff .
Loaded config default.
Using cache file ".php-cs-fixer.cache".

Checked all files in 0.032 seconds, 12.000 MB memory used

% ../tools/composer.phar run-script cs     
> ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix .
Loaded config default.
Using cache file ".php-cs-fixer.cache".

Fixed all files in 0.006 seconds, 12.000 MB memory used

接著,可以評估一下自己打算用哪些規範,從 PHP Framework Interop Group (簡稱PHP-FIG) 常見的推薦用法,就是要來個 PSR-12 就對了:

% cat test.php 
<?php

    $x                = "hello world";

     $y =            "123";

% ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --using-cache=no --rules=@PSR12  -vvv test.php
PHP CS Fixer 3.8.0 BerSzcz against war! by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 7.4.29
Loaded config default.
.                                                                                                                           1 / 1 (100%)
Legend: ?-unknown, I-invalid file syntax (file ignored), S-skipped (cached or empty file), .-no changes, F-fixed, E-error

Checked all files in 0.005 seconds, 12.000 MB memory used

規則嚴一點,使用 Symfony framework 語法:

% ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff --using-cache=no --rules=@PSR12,@Symfony -vvv test.php 
PHP CS Fixer 3.8.0 BerSzcz against war! by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 7.4.29
Loaded config default.
F                                                                                                                           1 / 1 (100%)
Legend: ?-unknown, I-invalid file syntax (file ignored), S-skipped (cached or empty file), .-no changes, F-fixed, E-error
   1) web/test.php (single_quote, binary_operator_spaces)
      ---------- begin diff ----------
--- /Volumes/Data/project/src/test.php
+++ /Volumes/Data/project/src/test.php
@@ -1,5 +1,5 @@
 <?php
 
-    $x                = "hello world";
+    $x = 'hello world';
 
-     $y =            "123";
+     $y = '123';

      ----------- end diff -----------


Checked all files in 0.011 seconds, 12.000 MB memory used

進行修正:

% ../tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --using-cache=no --rules=@PSR12,@Symfony test.php
Loaded config default.
   1) web/test.php

Fixed all files in 0.011 seconds, 12.000 MB memory used

% cat test.php 
<?php

    $x = 'hello world';

     $y = '123';

更多語法修正可以參考 github.com/FriendsOfPHP/PHP-CS-Fixer/blob/master/doc/rules/index.rst 解說,例如變數運算時的空白規劃,就可以參考 binary_operator_spaces 的設計。

2022年6月8日 星期三

樊登說書

圖:樊登說書 實體勳章 非同凡響

約莫 2021 春天起入坑,接著靠運動時期清書,應該沒八個月規律地刷完一遍,不到四百本書,正在進行複習第二遍,未來就進入每週聽一本書,然後沒啥 podcast 想聽時,繼續挑有興趣的來複習。

從樊登說書的熱門書籍清起,可以很清楚地看到 TA 已經是親子學習為主、商業工具書為輔,但無論如何,都仍是人性議題,帶團隊跟養小孩都還是相互呼應的,稱不上衝突。例如如何鼓勵員工往前邁進,等同於鼓勵小孩如何進步一樣。

我以前也覺得網路上有很多文料,舉凡工作技術文、親子、科普等,認為不需要翻書了,直到被大學室友提醒:片段的文章,跟從頭到尾的書籍,還是有很大的不一樣。現代人的習性也慢慢被切割成只能閱讀零碎的片段。

樊登說書其實厲害的就樊登一人,自己打通任督二脈,以論語中庸當內力,在吸取中內外的書籍,的確如他所言,這個事業搞起來後,自己受益最大,也開始慢跑健身少吃麵食,也持續享受著看書、講書的興趣,推掉不必要聚會,並精實的呼應著讓人一年讀 52本書,另外,也朝向不吃光用戶眼光跟時間,吸收完知識後,關掉 app 去做正經事。其實看一個人功力強不強,就像看廚師炒飯一樣,一本經典書籍內容那麼充沛,有幾位網紅可以好好用 30 分鐘介紹一本書?又有幾位網紅可以串起各類書籍,並且前後呼應而不是自相矛盾?

回顧起 2021 年接觸的 app ,應當就這 app 受益最大。從商務工具書,提醒自己溝通時要著重事實的呈述而非情緒,親子相處時,要多留點時間給小孩,不只陪伴,還包括等待小孩成長。我想,我還是會繼續複習完成二刷吧


樊登說書 熱門書籍 Top 100 清單(說書時間、書名):

2017-03-18 正面管教

2016-07-30 非暴力沟通

2017-04-08 亲密关系

2018-04-28 终身成长

2019-03-30 逆商

2018-12-15 父母的语言

2016-11-19 刻意练习

2015-10-24 关键对话

2017-12-09 可复制的领导力

2017-10-07 高效能人士的七个习惯

2017-01-07 如何让你爱的人爱上你

2018-06-09 即兴演讲

2017-05-20 自卑与超越

2016-01-01 如何培养孩子的社会能力

2019-03-02 应对焦虑

2018-10-27 活好

2013-12-03 你就是孩子最好的玩具

2019-06-01 不管教的勇气

2019-07-20 掌控习惯

2016-12-10 不吼不叫

2016-07-16 如何说孩子才会听,怎么听孩子才肯说

2019-01-12 感受爱

2015-09-18 幸福的婚姻

2018-11-03 认知天性

2020-04-18 陪孩子终身成长

2016-10-15 销售就是要玩转情商

2019-10-12 不妥协的谈判

2019-03-16 列奥纳多·达·芬奇传

2020-01-04 我们都曾受过伤,却有了更好的人生

2019-04-06 能力陷阱

2021-07-10 人生只有一件事

2019-06-15 自尊

2019-08-03 事实

2019-11-09 被忽视的孩子

2018-03-03 他人的力量

2019-04-27 低风险创业

2019-06-08 高效休息法

2020-02-29 思维的囚徒

2019-02-16 用事实说话

2019-05-04 坚毅

2020-09-19 自驱型成长

2020-09-26 活出心花怒放的人生

2018-12-08 思辨与立场

2013-11-25 幸福的方法

2016-02-04 我的情绪为何总被他人左右

2020-09-12 洞见

2018-01-06 反脆弱

2019-05-11 原生家庭

2018-07-07 活法

2019-08-31 解码青春期

2014-01-04 第3选择

2020-02-08 爱、金钱和孩子

2020-04-11 意志力

2018-10-13 掌控谈话

2018-10-06 运动改造大脑

2019-08-17 顾客为什么购买

2021-09-11 心态

2019-11-23 孔子传

2017-11-18 思考,快与慢

2019-04-20 世界观

2020-05-23 斯坦福高效睡眠法

2016-03-26 干法

2016-06-25 你要如何衡量你的人生

2018-08-11 热锅上的家庭

2015-07-03 次第花开

2021-05-22 解惑:心智模式决定你的一生

2018-09-22 一平方米的静心

2020-08-08 陌生人效应

2019-07-13 学会吃饭

2020-09-05 曾国藩的正面与侧面

2019-08-10 身为职场女性

2020-06-20 与内心的恐惧对话

2017-08-05 少有人走的路

2019-12-14 幸福的陷阱

2019-03-23 欲罢不能

2016-12-03 销售洗脑

2019-07-27 道德经说什么

2018-12-01 苏东坡传

2019-03-09 你要的是幸福,还是对错

2019-04-13 掌控

2021-04-17 你好,小孩

2020-11-28 内在动机:自主掌控人生的力量

2018-11-10 扫除道

2015-02-06 叛逆不是孩子的错

2021-05-15 考试脑科学

2018-01-27 深度工作

2018-03-24 心流

2021-04-24 如何克服社交焦虑

2019-02-23 高能量姿势

2019-09-07 分手后,成为更好的自己

2014-03-14 管理十诫

2021-03-20 幸福关系的7段旅程

2016-05-06 养育女孩

2020-07-25 学习的格局

2015-12-12 瞬变

2019-11-16 你为什么不道歉

2017-12-02 恰如其分的自尊

2017-01-21 王阳明哲学

2020-03-28 弹性

2019-05-18 有限与无限的游戏

2022年5月30日 星期一

Go 開發筆記 - 使用 Regular Expression / Regex / Re

網路上還滿多教學以及文件,其中官方文件 pkg.go.dev/regexp 滿清楚的。也有看到一篇許多人推的 github.com/StefanSchroeder/Golang-Regex-Tutorial ,有中英文。

在此把一些語法筆記一下,方便自己未來快速回憶。

使用筆記:
  • 有兩種 Regular Expression 實現方式 ,其中有 POSIX 關鍵字是 POSIX ERE (egrep) 語法跟效果。
  • 初始化有 Compile 跟 MustCompile (對應的是 CompilePOSIX 和 MustCompilePOSIX) ,其中有 Must 字眼是驗證語法錯誤時,會進入 panics 狀態
使用範例:

% cat main.go
package main

// https://pkg.go.dev/regexp
// https://pkg.go.dev/regexp/syntax

import (
    "fmt"
    "regexp"
)

func main() {
    input := `
<html>
    <head>
        <title>study golang</title>
    </head>
    <body>
        <ul>
            <li>changyy.org</li>
            <li>1234567890</li>
            <li>abcdefg</li>
            <li>abcdefgABCDEFG</li>
            <li>abcdef1234567890gABCDEFG</li>
        </ul>
    </body>
</html>`

    // Style 1: regexp.Match
    {
        matched, err := regexp.Match(`<[A-Za-z]+>(.*?)</[A-Za-z]+>`, []byte(input))
        if err != nil {
            fmt.Println("Style 1 - regexp.Match error:", err)
        } else {
            fmt.Println("Style 1 - regexp.Match: ", matched)
        }
    }

    // Style 2: regexp.Compile + obj.MatchString
    // https://pkg.go.dev/regexp#Regexp.MatchString
    {
        obj, err := regexp.Compile(`<[A-Za-z]+>(.*?)</[A-Za-z]+>`)
        if err != nil {
            fmt.Println("regexp.Compile error:", err)
        } else {
            fmt.Println("Style 2 - case 1: ", obj.MatchString(`<title>Hello World</title>`))
            fmt.Println("Style 2 - case 2: ", obj.MatchString(input))
        }
    }

    // Style 3: regexp.MustCompile + obj.MatchString
    // https://pkg.go.dev/regexp#Regexp.MatchString
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 3 - case 1: ", obj.MatchString(`<title>Hello World</title>`))
        fmt.Println("Style 3 - case 2: ", obj.MatchString(input))
    }

    // Style 4: obj.FindString
    // https://pkg.go.dev/regexp#Regexp.FindString
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 4 - case 1: ", obj.FindString(`<title>Hello World</title>`))
        fmt.Println("Style 4 - case 2: ", obj.FindString(input))
    }

    // Style 5: obj.FindAllString , obj.FindAllStringIndex
    // https://pkg.go.dev/regexp#Regexp.FindAllString
    // https://pkg.go.dev/regexp#Regexp.FindAllStringIndex
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 5 - case 1: ", obj.FindAllString(`<title>Hello World</title>`, -1))
        fmt.Println("Style 5 - case 2: ", obj.FindAllString(input, -1))
        fmt.Println("Style 5 - case 3: ", obj.FindAllStringIndex(`<title>Hello World</title>`, -1))
        fmt.Println("Style 5 - case 4: ", obj.FindAllStringIndex(input, -1))
    }

    // Style 6: obj.FindStringSubmatch , obj.FindStringSubmatchIndex
    // https://pkg.go.dev/regexp#Regexp.FindStringSubmatch
    // https://pkg.go.dev/regexp#Regexp.FindStringSubmatchIndex
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 6 - case 1: ", obj.FindStringSubmatch(`<title>Hello World</title>`))
        fmt.Println("Style 6 - case 2: ", obj.FindStringSubmatch(input))
        fmt.Println("Style 6 - case 3: ", obj.FindStringSubmatchIndex(`<title>Hello World</title>`))
        fmt.Println("Style 6 - case 4: ", obj.FindStringSubmatchIndex(input))
    }

    // Style 7: obj.ReplaceAllString
    // https://pkg.go.dev/regexp#Regexp.ReplaceAllString
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 7 - case 1: ", obj.ReplaceAllString(`<title>Hello World</title>`, "A"))
        fmt.Println("Style 7 - case 2: ", obj.ReplaceAllString(input, "B"))
    }

    // Style 8: obj.Split
    // https://pkg.go.dev/regexp#Regexp.Split
    {
        pattern := `<[A-Za-z]+>(.*?)</[A-Za-z]+>`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 8 - case 1: ", len(obj.Split(`<title>Hello World</title>`, -1)))
        fmt.Println("Style 8 - case 2: ", len(obj.Split(input, -1)))
    }

    // Style 9: obj.Longest()
    // https://pkg.go.dev/regexp#Regexp.FindString
    // https://pkg.go.dev/regexp#Regexp.Longest
    {
        pattern := `a(|b)`
        obj := regexp.MustCompile(pattern)
        fmt.Println("Style 9 - case 1: ", obj.FindString(`abb`))
        obj.Longest()
        fmt.Println("Style 9 - case 2: ", obj.FindString(`abb`))
    }
}

% go run main.go 
Style 1 - regexp.Match:  true
Style 2 - case 1:  true
Style 2 - case 2:  true
Style 3 - case 1:  true
Style 3 - case 2:  true
Style 4 - case 1:  <title>Hello World</title>
Style 4 - case 2:  <title>study golang</title>
Style 5 - case 1:  [<title>Hello World</title>]
Style 5 - case 2:  [<title>study golang</title> <li>changyy.org</li> <li>1234567890</li> <li>abcdefg</li> <li>abcdefgABCDEFG</li> <li>abcdef1234567890gABCDEFG</li>]
Style 5 - case 3:  [[0 26]]
Style 5 - case 4:  [[27 54] [103 123] [136 155] [168 184] [197 220] [233 266]]
Style 6 - case 1:  [<title>Hello World</title> Hello World]
Style 6 - case 2:  [<title>study golang</title> study golang]
Style 6 - case 3:  [0 26 7 18]
Style 6 - case 4:  [27 54 34 46]
Style 7 - case 1:  A
Style 7 - case 2:  
<html>
    <head>
        B
    </head>
    <body>
        <ul>
            B
            B
            B
            B
            B
        </ul>
    </body>
</html>
Style 8 - case 1:  2
Style 8 - case 2:  7
Style 9 - case 1:  a
Style 9 - case 2:  ab

2022年5月26日 星期四

Go 開發筆記 - 使用 database/sql 通用介面存取資料庫,以 SQLite3 為例

在 Golang 的世界,有定義資料庫存取的通用介面 database/sql ,但貌似官方沒有提供實作而是讓廣大的鄉民開發,並且標記哪些套件是有通過 go-sql-test 驗證的,因此,大部分就是挑哪些有標記的,或是直接看 github 有多熱門也行。

相關文件:
以下就連續動作,筆記一下。

程式碼:

package main

import (
    "log"
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "/tmp/sqlite3.db")
    if err != nil {
        log.Fatalln(err)
    }
    defer db.Close()

    //
    // http://go-database-sql.org/modifying.html
    //
    // via db.Exec with checking the error message only
    if _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS account (
            uid INTEGER PRIMARY KEY AUTOINCREMENT,
            username VARCHAR(64) NULL
        );
    `); err != nil {
        log.Println(err)
    }

    // Insert via db.Prepare
    if stmt, err := db.Prepare("INSERT INTO account(username) VALUES(?)"); err == nil {
        if res, err := stmt.Exec("changyy.org"); err != nil {
            log.Println("Insert Exec Error:", err)
        } else if lastId, err := res.LastInsertId() ; err != nil {
            log.Println("Get LastInsertId Error:", err)
        } else if rowCount, err := res.RowsAffected() ; err != nil {
            log.Println("Get RowsAffected Error:", err)
        } else {
            log.Println("Insert Done, Last Insert Id:", lastId, ", RowsAffected: ", rowCount)

            // Update via db.Prepare
            if stmt, err := db.Prepare("UPDATE account SET username = ? WHERE uid = ?"); err != nil {
                log.Println("Update Prepqre Error:", err)
            } else if res, err := stmt.Exec("blog.changyy.org", lastId); err != nil {
                log.Println("Update Exec Error:", err)
            } else if rowCount, err := res.RowsAffected() ; err != nil {
                log.Println("Get RowsAffected Error:", err)
            } else {
                log.Println("Update RowsAffected: ", rowCount)
            }
        }
    } else {
        log.Println("Prepqre Insert Error:", err)
    }

    //
    // http://go-database-sql.org/retrieving.html
    // https://pkg.go.dev/database/sql#DB.Query
    //
    // via db.Query with sql.Rows and error mesasge
    rows, err := db.Query("SELECT * FROM account")
    if err != nil {
        log.Println(err)
    } else {
        defer rows.Close()
        log.Println("Result:")
        for rows.Next() {
            var id int
            var username string
            if err := rows.Scan(&id, &username) ; err == nil {
                log.Println(id, username)
            } else {
                log.Println(err)
            }
        }
        if err := rows.Err() ; err != nil {
            log.Println(err)
        }
    }
}

執行:

% go run main.go    
2022/05/25 20:44:26 Insert Done, Last Insert Id: 1 , RowsAffected:  1
2022/05/25 20:44:26 Update RowsAffected:  1
2022/05/25 20:44:26 Result:
2022/05/25 20:44:26 1 blog.changyy.org

% file /tmp/sqlite3.db
/tmp/sqlite3.db: SQLite 3.x database, last written using SQLite version 3038005, file counter 3, database pages 3, cookie 0x1, schema 4, UTF-8, version-valid-for 3

% sqlite3 /tmp/sqlite3.db .schema
CREATE TABLE account (
            uid INTEGER PRIMARY KEY AUTOINCREMENT,
            username VARCHAR(64) NULL
        );
CREATE TABLE sqlite_sequence(name,seq);