Composer là gì?
Giống như NPM đối với JavaScript, Composer là công cụ dành riêng cho PHP giúp bạn quản lý các thư viện phụ thuộc cho ứng dụng của mình. Nếu bạn đã sử dụng một framework như Symfony, chắc chắn bạn đã sử dụng nó. Trên thực tế, mọi nhà phát triển PHP đều sử dụng nó hàng ngày. Nếu bạn đang ở trang này, có lẽ bạn đang tự hỏi làm thế nào để tạo một thư viện có thể dễ dàng phân phối và chia sẻ với cộng đồng. Đó là điều mà tôi sẽ cố gắng chỉ cho bạn.
Vì lợi ích của hướng dẫn này, tôi sẽ lấy một ví dụ cụ thể về một bản sửa lỗi mà tôi đã phải áp dụng cho một dự án gần đây. Tôi thực sự tự hỏi, "Tại sao chưa có công cụ nào để sửa chữa điều này một cách dễ dàng?". Vì vậy, tôi sẽ biến bản sửa lỗi nhỏ này thành một thư viện Composer và có thể, ngoài việc giúp tôi viết bài viết này, thư viện này cũng sẽ hữu ích cho người khác.
Context: Tôi thừa hưởng một dự án với vấn đề về bảo mật dữ liệu nhập vào cơ sở dữ liệu. Nhà phát triển đã có ý tưởng tốt khi sử dụng serialize trên một mảng. Tốt, đó không phải là điều tôi sẽ làm, nhưng đó là cách ứng dụng được tạo ra và bản thân nó không quá phiền phức. Trừ khi rõ ràng, dữ liệu đã được chuyển từ DB này sang DB khác (mysql->mssql->mysql) tạo ra vấn đề mã hóa. Chúng ta kết thúc với một lỗi unserialize error at offset đẹp đẽ. Lỗi này khá chi tiết. Định dạng serialize của PHP, chỉ ra loại tiếp theo là số lượng ký tự, cho phép chúng ta nhanh chóng chẩn đoán rằng số lượng ký tự được tính toán không chính xác. Chỉ cần tính toán lại và viết lại trong tệp với regex và một callback.
Làm nhắc nhở, đây là tài liệu về định dạng https://en.wikipedia.org/wiki/PHP_serialization_format

Vì vậy, lỗi của chúng ta đến từ quá trình bảo mật lại khi số lượng ký tự được chỉ định cho một chuỗi không khớp với những gì được giải mã bởi hàm unserialize. Vì vậy, bạn phải tính toán lại. Ví dụ:
s:6:"apple";
phải là:
s:5:"apple";
Khá đơn giản, nhưng hoàn hảo cho ví dụ của chúng ta!
Chúng ta bắt đầu thôi! Chúng ta sẽ làm bẩn tay.
Điều kiện tiên quyết
Chúng ta sẽ cần:
– PHP được cài đặt trên CLI trên máy của chúng ta,
– một tài khoản trên github,
– một tài khoản trên packagist
– Composer được cài đặt trên máy của chúng ta.
Có lẽ, nếu bạn ở đây thì bạn đã cài đặt PHP và Composer. Nếu chưa, bạn có thể tìm thấy rất nhiều tài nguyên trên Internet để giải thích cách làm.
Vì vậy, chúng ta hãy bắt đầu bằng cách tạo dự án của mình dưới trình biên tập yêu thích của chúng ta, đối với tôi là PhpStorm. Tại giai đoạn này, bạn nên có một thư mục trống.
Lệnh sau đây sẽ giúp bạn khởi tạo dự án Composer của mình và bổ sung nó với thông tin cơ bản như Giấy phép, tên, địa chỉ kho lưu trữ, và mô tả ngắn gọn.
composer init -q -a src \
--name partitech/fix-serialize \
--description "Fix Php unserialize error at offset" \
--license MIT
Bạn sẽ chuyển từ một thư mục trống sang một kiến trúc được thiết kế cho Composer. Điều này khá diệu kỳ.

{
"name": "partitech/fix-serialize",
"description": "Fix Php unserialize error at offset",
"license": "MIT",
"autoload": {
"psr-4": {
"Partitech\\FixSerialize\\": "src"
}
},
"require": {}
}
src: Chứa mã của bạn
vendor: Chứa tất cả các phụ thuộc. Vâng, thư viện của bạn có thể cần các thư viện Composer khác để hoạt động.
composer.json và composer.lock là các tệp bạn chắc chắn quen thuộc 😉
Như mọi thư viện tốt, chúng ta sẽ kiểm tra mã của mình ở mọi giai đoạn của quá trình phát triển, và để làm điều này chúng ta sẽ cài đặt, đoán xem... PhpUnit.
composer require --dev phpunit/phpunit ^9.5
Chúng ta sẽ cấu hình PhpUnit với phpunit.xml nổi tiếng và thêm các tham chiếu phù hợp vào composer.json.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="common tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Tệp composer của bạn nên trông giống như thế này.
{
"name": "partitech/fix-unserialize",
"description": "Fix Php unserialize error at offset",
"license": "MIT",
"autoload": {
"psr-4": {
"Partitech\\FixUnSerialize\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5"
},
"scripts": {
"test": "phpunit"
}
}
Bạn có thể tạo thư mục tests và chạy lệnh composer test. Hiện tại không có bài kiểm tra nào, nhưng điều này sẽ xác nhận rằng mọi thứ đã được thiết lập đúng cách.
mkdir tests
composer test

Đã đến lúc tạo bài kiểm tra giả đầu tiên của chúng ta, điều này sẽ giúp chúng ta tạo các tệp cần thiết.
<?php
namespace Partitech\FixUnSerialize\Tests;
use Partitech\FixUnSerialize\UnSerializeService;
class FixUnSerializeTest extends \PHPUnit\Framework\TestCase
{
public function testSayHelloWorld()
{
$helloWorld = new UnSerializeService();
$this->assertEquals("Hello World!", $helloWorld->unserialize('Hello'));
}
}
<?php
namespace Partitech\FixUnSerialize;
class UnSerializeService
{
/**
* @param string $data
* @return string|null
*/
public function unserialize(string $data): ?string
{
return $data . ' World!';
}
}

Tại thời điểm này, bạn đã có đủ điều kiện để bắt đầu viết mã cho thư viện của riêng mình. Đối với tôi, tôi sẽ tạo các phương thức nhỏ và đơn giản nhất có thể để tôi có thể kiểm tra chúng một cách dễ dàng nhất. Tôi khá thích nguyên tắc "một phương thức, một trách nhiệm," cho phép tôi phân chia rõ ràng những gì tôi muốn làm. Ngoài ra, gán một trách nhiệm đơn giản cho mỗi phương thức của tôi cho phép tôi bao gồm hợp đồng/tài liệu trực tiếp trong tên. Như vậy, một phương thức "isValid" sẽ chỉ có trách nhiệm kiểm tra xem một mục nhập có hợp lệ hay không, v.v. Đây là sơ đồ của lớp chúng ta sẽ sử dụng. Một lần nữa, điều này chỉ để minh họa cho hướng dẫn của chúng ta.
public isValid(string $data): bool
public fixIfInvalid(string $data):? string
public fix(string $data): string
public fixLength(array $values): string
public unserialize(string $data)
Vì vậy, theo thứ tự, chúng ta gọi unserialize -> chúng ta kiểm tra nếu không hợp lệ -> chúng ta sửa chữa -> chúng ta bảo mật lại. Rất đơn giản thật. Tôi cung cấp mã cho ví dụ, và chúng ta tiếp tục.
<?php
namespace Partitech\FixUnSerialize;
class UnSerializeService
{
/**
* @param string $data
* @return mixed
*/
public function unserialize(string $data)
{
$data = $this->fixIfInvalid($data);
return unserialize($data);
}
/**
* @param string $data
* @return string|null
*/
public function fixIfInvalid(string $data): ?string
{
if (!$this->isValid($data)) {
$data = $this->fix($data);
}
return $data;
}
/**
* @param string $data
* @return bool
*/
public function isValid(string $data): bool
{
if (!@unserialize($data)) {
return false;
}
return true;
}
/**
* @param string $data
* @return string
*/
public function fix(string $data): string
{
$pattern = '/s\:(\d+)\:\"(.*?)\";/s';
return preg_replace_callback($pattern, [$this, 'fixLength'], $data);
}
/**
* @param array $values
* @return string
*/
public function fixLength(array $values): string
{
$string = $values[2];
$length = strlen($string);
return 's:' . $length . ':"' . $string . '";';
}
}
<?php
namespace Partitech\FixUnSerialize\Tests;
use Partitech\FixUnSerialize\UnSerializeService;
use PHPUnit\Framework\TestCase;
class FixUnSerializeTest extends TestCase
{
const VALID_STRING = 'a:2:{s:4:"test";s:4:"test";s:5:"test2";s:5:"test2";}';
const INVALID_STRING = 'a:2:{s:123456:"test";s:4:"test";s:5:"test2";s:5:"test2";}';
private UnSerializeService $unserializeService;
public function __construct(?string $name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->unserializeService = new UnSerializeService();
}
public function testIsValidTrue()
{
$this->assertTrue($this->unserializeService->isValid(self::VALID_STRING));
}
public function testIsValidFalse()
{
$this->assertNotTrue($this->unserializeService->isValid(self::INVALID_STRING));
}
public function testFixIfInvalidWithValidString()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fixIfInvalid(self::VALID_STRING)
);
}
public function testFixIfInvalidWithInvalidString()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fixIfInvalid(self::INVALID_STRING)
);
}
public function testFixLength()
{
$data = [
's:5000:"test2";',
5,
'test2'
];
$expected = 's:5:"test2";';
$this->assertEquals(
$expected,
$this->unserializeService->fixLength($data)
);
}
public function testFixValid()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fix(self::VALID_STRING)
);
}
public function testFixInvalid()
{
$this->assertEquals(
self::VALID_STRING,
$this->unserializeService->fix(self::INVALID_STRING)
);
}
public function testUnserializeValid()
{
$this->assertEquals(
unserialize(self::VALID_STRING),
$this->unserializeService->unserialize(self::VALID_STRING)
);
}
public function testUnserializeInvalid()
{
$this->assertEquals(
unserialize(self::VALID_STRING),
$this->unserializeService->unserialize(self::INVALID_STRING)
);
}
}
Bây giờ chúng ta có mã của mình, chúng ta sẽ đẩy nó lên Github. Chúng ta bắt đầu bằng việc phiên bản hóa dự án của mình và thêm danh sách các tệp mà chúng ta không muốn bao gồm.
git init
touch .gitignore
echo "vendor/" >> .gitignore
echo ".phpunit.result.cache" >> .gitignore
echo ".idea/" >> .gitignore
git add .
git commit -m "First commit"
Khởi tạo một kho lưu trữ trống trên Github. Chúng ta giờ có thể đẩy mã của mình lên kho lưu trữ của mình.
git remote add origin ssh://git@github.com/partitech/fix-serialize.git
git branch -M main
git push -f -u origin main
Bây giờ mã của bạn đã được xuất bản trên Github chúng ta có thể tiếp tục.

Tạo bản phát hành đầu tiên của bạn.

Trên trang tạo bản phát hành, chọn "Chọn một tag" và nhập "v0.0.1" và nhấp vào "tạo tag mới", tương tự cho tên bản phát hành của bạn. Nhập "Bản phát hành ban đầu" làm mô tả và nhấp vào "Xuất bản bản phát hành".

Một khi đã tạo, bạn sẽ ở trên trang bản phát hành đầu tiên của bạn.

Bước cuối cùng của chúng ta sẽ bao gồm việc tham chiếu phiên bản của chúng ta trên Packagist để nó có thể truy cập trực tiếp qua Composer.
Trên trang https://packagist.org/login/ sử dụng tùy chọn "đăng nhập bằng Github".

Nộp kho lưu trữ của bạn:



Bây giờ bạn đã có quyền truy cập vào gói của mình trực tiếp qua:
composer require partitech/fix-unserialize
Tuyệt vời 😉
Chúng ta sẽ cấu hình Github để thông báo cho Packagist về bất kỳ thay đổi nào đối với kho lưu trữ.
Lấy mã API Token của bạn trên Packagist https://packagist.org/profile/. Nhấp vào Hiển thị mã API và sao chép khóa của bạn.

Trong GitHub, vào Cài đặt > Webhooks > Chỉnh sửa.

Thay đổi Secret bằng cách dán khóa API Packagist của bạn.


Và voila, bạn có thể thực hiện push trên kho lưu trữ của mình và kiểm tra nhật ký trong Cài đặt > Webhooks > Giao hàng gần đây

Đảm bảo mọi thứ đều ổn trên Packagist.

Bạn giờ đã biết cách tạo một thư viện Composer và cách giao nó cho phần còn lại của thế giới 😊