Vietnamese (Tiếng Việt) translation by Andrea Ho (you can also view the original English article)
Việc cài đặt tất cả các thư viện PHP của bạn với Composer là cách tuyệt vời để tiết kiệm thời gian. Nhưng các dự án lớn hơn sẽ tự động được kiểm tra và chạy ở mỗi lần commit với hệ thống kiểm soát phiên bản phần mềm (SVC) của bạn sẽ mất nhiều thời gian để cài đặt tất cả các package cần thiết từ Internet. Bạn muốn chạy test ngay khi có thể thông qua hệ thống tích hợp liên tục (CI) để bạn có phản hồi nhanh và nhanh chóng tác động lại các thất bại. Trong hướng dẫn này, chúng tôi sẽ thiết lập bản sao cục bộ để ủy quyền tất cả các package của bạn được yêu cầu trong file composer.json
của dự án. Điều này sẽ giúp CI của chúng tôi hoạt động nhanh hơn nhiều, cài đặt các package qua mạng cục bộ hoặc thậm chí được lưu trữ trên cùng một máy và đảm bảo chúng tôi luôn có sẵn phiên bản cụ thể của các package.
Satis là gì?
Satis là tên của ứng dụng mà chúng tôi sẽ sử dụng để nhân bản các repo khác nhau cho dự án của chúng tôi. Nó đóng vai trò như một proxy giữa Internet và composer của bạn. Giải pháp của chúng tôi sẽ tạo ra một bản sao cục bộ của một vài package và hướng dẫn composer của chúng tôi sử dụng nó thay vì các nguồn được tìm thấy trên Internet.
Nói có sách mách có chứng.

Dự án của chúng tôi sẽ sử dụng composer như bình thường. Nó sẽ được cấu hình để sử dụng máy chủ Satis cục bộ làm nguồn chính. Nếu một package được tìm thấy ở đó, nó sẽ được cài đặt từ đó. Nếu không, chúng tôi sẽ cho phép composer sử dụng packagist.org mặc định để truy xuất package.
Lấy về Satis
Satis có sẵn thông qua composer, vì vậy việc cài đặt nó rất đơn giản. Trong kho lưu trữ mã nguồn đính kèm, bạn sẽ tìm thấy Satis được cài đặt trong thư mục Sources/Satis
. Đầu tiên chúng ta cài đặt trình composer.
$ curl -sS https://getcomposer.org/installer | php #!/usr/bin/env php All settings correct for using Composer Downloading... Composer successfully installed to: /home/csaba/Personal/Programming/NetTuts/Setting up a local mirror for Composer packages with Satis/Sources/Satis/composer.phar Use it: php composer.phar
Sau đó, chúng tôi sẽ cài đặt Satis.
$ php composer.phar create-project composer/satis --stability=dev --keep-vcs Installing composer/satis (dev-master eddb78d52e8f7ea772436f2320d6625e18d5daf5) - Installing composer/satis (dev-master master) Cloning master Created project in /home/csaba/Personal/Programming/NetTuts/Setting up a local mirror for Composer packages with Satis/Sources/Satis/satis Loading composer repositories with package information Installing dependencies (including require-dev) from lock file - Installing symfony/process (dev-master 27b0fc6) Cloning 27b0fc645a557b2fc7bc7735cfb05505de9351be - Installing symfony/finder (v2.4.0-BETA1) Downloading: 100% - Installing symfony/console (dev-master f44cc6f) Cloning f44cc6fabdaa853335d7f54f1b86c99622db518a - Installing seld/jsonlint (1.1.1) Downloading: 100% - Installing justinrainbow/json-schema (1.1.0) Downloading: 100% - Installing composer/composer (dev-master f8be812) Cloning f8be812a496886c84918d6dd1b50db5c16da3cc3 - Installing twig/twig (v1.14.1) Downloading: 100% symfony/console suggests installing symfony/event-dispatcher () Generating autoload files
Cấu hình Satis
Satis được cấu hình bởi một file JSON rất giống với Composer. Bạn có thể sử dụng tên mà bạn muốn cho file của mình và chỉ định tên đó để sử dụng sau này. Chúng tôi sẽ sử dụng "mirrored-package.conf
".
{ "name": "NetTuts Composer Mirror", "homepage": "https://localhost:4680", "repositories": [ { "type": "vcs", "url": "https://github.com/SynetoNet/monolog" }, { "type": "composer", "url": "https://packagist.org" } ], "require": { "monolog/monolog": "syneto-dev", "mockery/mockery": "*", "phpunit/phpunit": "*" }, "require-dependencies": true }
Hãy phân tích file cấu hình này.
-
name
- đại diện cho một string sẽ được hiển thị trên giao diện web của máy nhân bản của chúng tôi. -
homepage
- là địa chỉ web sẽ lưu trữ các package của chúng tôi. Điều này không cho máy chủ web của chúng tôi sử dụng địa chỉ và cổng đó, nó chỉ là thông tin của cấu hình hoạt động. Chúng tôi sẽ thiết lập quyền truy xuất vào nó trên địa chỉ và cổng đó sau. -
repositories
- một danh sách các repo theo thứ tự ưu tiên. Trong ví dụ của chúng tôi, kho lưu trữ đầu tiên là một ngã ba Github của các thư viện ghi nhật ký độc thoại. Nó có một số sửa đổi và chúng tôi muốn sử dụng fork cụ thể đó khi cài đặt monolog. Loại repo này là "vcs
". Repo thứ hai thuộc kiểu "composer
". URL của nó là trang mặc định của packagist.org. -
require
- liệt kê các package chúng tôi muốn nhân bản. Nó có thể đại diện cho một package cụ thể với một phiên bản hoặc branch cụ thể hoặc phiên bản bất kỳ. Nó sử dụng cú pháp giống như "require
" hoặc "require-dev
" trongcomposer.json
của bạn. -
require-dependencies
- là tùy chọn cuối cùng trong ví dụ của chúng tôi. Nó sẽ báo Satis sao chép các package chúng tôi đã chỉ định trong phần "require
" và tất cả các phụ thuộc của chúng.
Để nhanh chóng thử các cài đặt của chúng tôi, trước tiên chúng tôi cần nói với Satis để tạo các bản sao. Chạy lệnh này trong thư mục bạn đã cài đặt Satis.
$ php ./satis/bin/satis build ./mirrored-packages.conf ./packages-mirror Scanning packages Writing packages.json Writing web view
Trong khi quá trình đang diễn ra, bạn sẽ thấy Satis nhân bản từng phiên bản được tìm thấy của các package yêu cầu. Hãy kiên nhẫn vì có thể mất thời gian để xây dựng tất cả các package đó.
Satis yêu cầu date.timezone
đó phải được xác định trong file php.ini
, vì vậy hãy đảm bảo rằng nó được thiết lập và xét thành múi giờ địa phương của bạn. Nếu không, lỗi sẽ xuất hiện.
[Twig_Error_Runtime] An exception has been thrown during the rendering of a template ("date_default_timezone_get(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set) function.
Sau đó, chúng ta có thể chạy một cá thể máy chủ PHP trong bảng điều khiển của chúng tôi trỏ đến repo được tạo gần đây. Bắt buộc bản PHP 5.4 hoặc mới hơn.
$ php -S localhost:4680 -t ./packages-mirror/ PHP 5.4.22-pl0-gentoo Development Server started at Sun Dec 8 14:47:48 2013 Listening on http://localhost:4680 Document root is /home/csaba/Personal/Programming/NetTuts/Setting up a local mirror for Composer packages with Satis/Sources/Satis/packages-mirror Press Ctrl-C to quit. [Sun Dec 8 14:48:09 2013] 127.0.0.1:56999 [200]: / [Sun Dec 8 14:48:09 2013] 127.0.0.1:57000 [404]: /favicon.ico - No such file or directory
Và bây giờ chúng tôi có thể duyệt các package đã nhân đôi và thậm chí tìm kiếm các package cụ thể bằng cách trỏ trình duyệt web của chúng tôi tới http://localhost:4680
:

Hãy host (lưu) nó trên Apache
Nếu bạn có Apache đang chạy, việc tạo một máy chủ ảo cho Satis sẽ khá đơn giản.
Listen 4680 <Directory "/path/to/your/packages-mirror"> Options -Indexes FollowSymLinks AllowOverride all Order allow,deny Allow from all </Directory> <VirtualHost *:4680> DocumentRoot "/path/to/your/packages-mirror" ServerName 127.0.0.1:4680 ServerAdmin admin@example.com ErrorLog syslog:user </VirtualHost>
Chúng tôi chỉ sử dụng một file .conf
như thế này, đặt trong thư mục conf.d
của Apache, thường là /etc/apache2/conf.d
. Nó tạo ra một máy chủ ảo trên cổng 4680 và trỏ nó vào thư mục của chúng tôi. Tất nhiên bạn có thể sử dụng bất cứ cổng nào bạn muốn.
Cập nhật các bản sao của chúng tôi
Satis không thể tự động cập nhật các bản sao trừ khi chúng ta yêu cầu. Vì vậy, cách dễ nhất, trên bất kỳ hệ thống UNIX nào, là chỉ cần thêm một cron job (tác vụ đình kỳ) vào hệ thống của bạn. Điều đó sẽ rất dễ dàng và chỉ là script đơn giản để thực hiện lệnh cập nhật.
#!/bin/bash php /full/path/to/satis/bin/satis build \ /full/path/to/mirrored-packages.conf \ /full/path/to/packages-mirror
Hạn chế của giải pháp này là nó không thay đổi. Chúng tôi phải cập nhật thủ công mirrored-packages.conf
mỗi khi chúng tôi bổ sung package khác vào composer.json
của dự án. Nếu bạn là thành viên của một nhóm trong một công ty có dự án lớn và máy chủ tích hợp liên tục, bạn không thể trông cậy mọi người nhớ đến việc thêm các package trên máy chủ. Họ thậm chí có thể không có quyền truy xuất vào cơ sở hạ tầng CI.
Tự động cập nhật cấu hình Satis
Đã đến lúc thực hành TDD PHP. Nếu bạn chỉ muốn code của mình sẵn sàng và chạy, hãy xem mã nguồn được đính kèm với hướng dẫn này.
require_once __DIR__ . '/../../../../vendor/autoload.php'; class SatisUpdaterTest extends PHPUnit_Framework_TestCase { function testBehavior() { $this->assertTrue(true); } }
Như thường lệ, chúng tôi bắt đầu với một test, chỉ đủ để đảm bảo rằng chúng tôi có một framework kiểm tra hiệu quả. Bạn có thể nhận thấy rằng tôi có một dòng request_once khá lạ, điều này là do tôi muốn tránh phải cài đặt lại PHPUnit và Mockery cho mỗi dự án nhỏ. Vì vậy, tôi để chúng trong thư mục vendor
trong thư mục gốc của NetTuts
. Bạn chỉ nên cài đặt chúng với Composer và bỏ dòng request_once
hoàn toàn.
class SatisUpdaterTest extends PHPUnit_Framework_TestCase { function testDefaultConfigFile() { $expected = '{ "name": "NetTuts Composer Mirror", "homepage": "http://localhost:4680", "repositories": [ { "type": "vcs", "url": "https://github.com/SynetoNet/monolog" }, { "type": "composer", "url": "https://packagist.org" } ], "require": { }, "require-dependencies": true }'; $actual = $this->parseComposerConf(''); $this->assertEquals($expected, $actual); } }
Điều đó có vẻ đúng. Tất cả các trường ngoại trừ "require
" là tĩnh. Chúng ta chỉ cần tạo các package. Các repo đang trỏ vào bản sao private git của chúng tôi và đến packagist khi cần thiết. Quản lý các repo đó là công việc có hơi hướng sysadmin hơn là một nhà phát triển phần mềm.
Tất nhiên điều này thất bại với:
PHP Fatal error: Call to undefined method SatisUpdaterTest::parseComposerConf()
Việc sửa đối chúng dễ dàng.
private function parseComposerConf($string) { }
Tôi vừa thêm một phương thức rỗng với tên được yêu cầu là private, vào class test của chúng ta. Thật tuyệt, nhưng bây giờ chúng tôi có một lỗi khác.
PHPUnit_Framework_ExpectationFailedException : Failed asserting that null matches expected '{ ... }'
Do đó, null không khớp với string của chúng tôi chứa tất cả cấu hình mặc định đó.
private function parseComposerConf($string) { return '{ "name": "NetTuts Composer Mirror", "homepage": "http://localhost:4680", "repositories": [ { "type": "vcs", "url": "https://github.com/SynetoNet/monolog" }, { "type": "composer", "url": "https://packagist.org" } ], "require": { }, "require-dependencies": true }'; }
OK, hiệu quả. Tất cả bài test sẽ hoàn tất.
PHPUnit 3.7.28 by Sebastian Bergmann. Time: 15 ms, Memory: 2.50Mb OK (1 test, 1 assertion)
Nhưng chúng tôi đã giới thiệu một bản sao tệ hại. Tất cả các nội dung tĩnh ở hai nơi, viết từng chữ ở hai nơi khác nhau. Hãy sửa lỗi này:
class SatisUpdaterTest extends PHPUnit_Framework_TestCase { static $DEFAULT_CONFIG = '{ "name": "NetTuts Composer Mirror", "homepage": "http://localhost:4680", "repositories": [ { "type": "vcs", "url": "https://github.com/SynetoNet/monolog" }, { "type": "composer", "url": "https://packagist.org" } ], "require": { }, "require-dependencies": true }'; function testDefaultConfigFile() { $expected = self::$DEFAULT_CONFIG; $actual = $this->parseComposerConf(''); $this->assertEquals($expected, $actual); } private function parseComposerConf($string) { return self::$DEFAULT_CONFIG; } }
À! Thế thì tốt hơn.
function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration() { $expected = self::$DEFAULT_CONFIG; $actual = $this->parseComposerConf('{"require": {}}'); $this->assertEquals($expected, $actual); }
Tốt. Việc đó cũng qua rồi. Nhưng nó cũng nêu bật lên một số trùng lặp và phân bổ vô ích.
function testDefaultConfigFile() { $actual = $this->parseComposerConf(''); $this->assertEquals(self::$DEFAULT_CONFIG, $actual); } function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration() { $actual = $this->parseComposerConf('{"require": {}}'); $this->assertEquals(self::$DEFAULT_CONFIG, $actual); }
Chúng tôi đã gạch vào biến $expect
. $actual
cũng có thể được nội tuyến, nhưng tôi thích nó tốt hơn theo cách này. Nó duy trì tập trung vào những gì được test.
Bây giờ chúng tôi có một vấn đề khác. Bài test tiếp theo tôi muốn viết sẽ như thế này:
function testARequiredPackageInComposerWillBeInSatisAlso() { $actual = $this->parseComposerConf( '{"require": { "Mockery/Mockery": ">=0.7.2" }}'); $this->assertContains('"Mockery/Mockery": ">=0.7.2"', $actual); }
Nhưng sau khi viết cách triển khai đơn giản, chúng ta sẽ nhận thấy nó yêu cầu json_decode()
và json_encode()
. Và tất nhiên các hàm này định dạng lại chuỗi của chúng tôi và việc tìm khớp chuỗi sẽ khó khăn nhất. Chúng ta phải chậm lại một chút.
function testDefaultConfigFile() { $actual = $this->parseComposerConf(''); $this->assertJsonStringEqualsJsonString($this->jsonRecode(self::$DEFAULT_CONFIG), $actual); } function testEmptyRequiredPackagesInComposerJsonWillProduceDefaultConfiguration() { $actual = $this->parseComposerConf('{"require": {}}'); $this->assertJsonStringEqualsJsonString($this->jsonRecode(self::$DEFAULT_CONFIG), $actual); } private function parseComposerConf($jsonConfig) { return $this->jsonRecode(self::$DEFAULT_CONFIG); } private function jsonRecode($json) { return json_encode(json_decode($json, true)); }
Chúng tôi đã thay đổi phương thức xác nhận của mình để so sánh các chuỗi JSON và chúng tôi cũng đã mã hóa lại biến $actual
của chúng tôi. ParseComposerConf()
cũng được thay đổi đổi để sử dụng phương thức này. Bạn sẽ nhanh chóng nó có ích thế nào. Test tiếp theo của chúng tôi sẽ cụ thể hơn về JSON.
function testARequiredPackageInComposerWillBeInSatisAlso() { $actual = $this->parseComposerConf( '{"require": { "Mockery/Mockery": ">=0.7.2" }}'); $this->assertEquals('>=0.7.2', json_decode($actual, true)['require']['Mockery/Mockery']); }
Và làm cho test này vượt qua, cùng với phần còn lại của các test, lại khá dễ dàng.
private function parseComposerConf($jsonConfig) { $addedConfig = json_decode($jsonConfig, true); $config = json_decode(self::$DEFAULT_CONFIG, true); if (isset($addedConfig['require'])) { $config['require'] = $addedConfig['require']; } return json_encode($config); }
Chúng tôi lấy chuỗi JSON nhập vào, decode (giải mã) nó và nếu nó chứa field "require
", chúng tôi sẽ sử dụng chuỗi đó trong file cấu hình Satis của chúng tôi. Nhưng chúng tôi có thể muốn sao chép tất cả các phiên bản của một package, không chỉ phiên bản cuối cùng. Vì vậy, có lẽ chúng tôi muốn sửa đổi test của mình để kiểm tra xem phiên bản có phải là "*" trong Satis hay không, bất kể phiên bản chính xác là gì trong composer.json
.
function testARequiredPackageInComposerWillBeInSatisAlso() { $actual = $this->parseComposerConf( '{"require": { "Mockery/Mockery": ">=0.7.2" }}'); $this->assertEquals('*', json_decode($actual, true)['require']['Mockery/Mockery']); }
Điều đó rõ ràng thất bại với một thông điệp tuyệt vời:
PHPUnit_Framework_ExpectationFailedException : Failed asserting that two strings are equal. Expected :* Actual :>=0.7.2
Bây giờ, chúng ta cần thực sự chỉnh sửa JSON của mình trước khi encode (mã hóa).
private function parseComposerConf($jsonConfig) { $addedConfig = json_decode($jsonConfig, true); $config = json_decode(self::$DEFAULT_CONFIG, true); $config = $this->addNewRequires($addedConfig, $config); return json_encode($config); } private function toAllVersions($config) { foreach ($config['require'] as $package => $version) { $config['require'][$package] = '*'; } return $config; } private function addNewRequires($addedConfig, $config) { if (isset($addedConfig['require'])) { $config['require'] = $addedConfig['require']; $config = $this->toAllVersions($config); } return $config; }
Để vượt qua test, chúng tôi phải lặp lại qua từng phần tử của mảng package được yêu cầu và đặt phiên bản của chúng thành '*'. Xem phương thức toAllVersion()
để biết thêm chi tiết. Và để tăng tốc hướng dẫn này một chút, chúng tôi cũng trích xuất một số phương thức private trong cùng bước này. Bằng cách này, parseComoserConf()
có tính mô tả và dễ hiểu. Chúng tôi cũng có thể nội tuyến $config
vào các đối số của addNewRequires()
, nhưng vì lý do thẩm mỹ, tôi đã bố trí nó trên hai dòng.
Nhưng còn "require-dev
" trong composer.json
thì sao?
function testARquiredDevPackageInComposerWillBeInSatisAlso() { $actual = $this->parseComposerConf( '{"require-dev": { "Mockery/Mockery": ">=0.7.2", "phpunit/phpunit": "3.7.28" }}'); $this->assertEquals('*', json_decode($actual, true)['require']['Mockery/Mockery']); $this->assertEquals('*', json_decode($actual, true)['require']['phpunit/phpunit']); }
Điều đó rõ ràng là thất bại. Chúng tôi có thể làm cho nó vượt qua chỉ bằng cách sao chép/dán nếu điều kiện if của chúng tôi trong addNewRequires()
:
private function addNewRequires($addedConfig, $config) { if (isset($addedConfig['require'])) { $config['require'] = $addedConfig['require']; $config = $this->toAllVersions($config); } if (isset($addedConfig['require-dev'])) { $config['require'] = $addedConfig['require-dev']; $config = $this->toAllVersions($config); } return $config; }
Đúng, điều đó giúp nó vượt qua, nhưng điều đó trùng lặp nếu các mệnh đề if là khó chịu. Hãy xử lý chúng.
private function addNewRequires($addedConfig, $config) { $config = $this->addRequire($addedConfig, 'require', $config); $config = $this->addRequire($addedConfig, 'require-dev', $config); return $config; } private function addRequire($addedConfig, $string, $config) { if (isset($addedConfig[$string])) { $config['require'] = $addedConfig[$string]; $config = $this->toAllVersions($config); } return $config; }
Chúng tôi có thể vui vẻ trở lại, các test có màu xanh và chúng tôi đã cấu trúc lại code của chúng tôi. Tôi nghĩ chỉ còn làm thêm một test. Điều gì xảy ra nếu chúng ta có cả hai phần "require
" và "require-dev
" trong composer.json
?
function testItCanParseComposerJsonWithBothSections() { $actual = $this->parseComposerConf( '{"require": { "Mockery/Mockery": ">=0.7.2" }, "require-dev": { "phpunit/phpunit": "3.7.28" }}'); $this->assertEquals('*', json_decode($actual, true)['require']['Mockery/Mockery']); $this->assertEquals('*', json_decode($actual, true)['require']['phpunit/phpunit']); }
Điều đó không thành công vì các package được thiết lập bởi "require-dev
" sẽ ghi đè lên các package "require
" và chúng tôi sẽ gặp lỗi:
Undefined index: Mockery/Mockery
Chỉ cần thêm một dấu cộng để hợp nhất các array, và chúng ta đã hoàn thành.
private function addRequire($addedConfig, $string, $config) { if (isset($addedConfig[$string])) { $config['require'] += $addedConfig[$string]; $config = $this->toAllVersions($config); } return $config; }
Các test sắp hoàn thành. Logic của chúng tôi đã hoàn tất. Tất cả những gì chúng ta phải làm là trích xuất các phương thức vào file và class của riêng chúng. Phiên bản cuối cùng của các test và class SatisUpdater
có thể được tìm thấy trong mã nguồn đính kèm.
Bây giờ chúng ta có thể sửa đổi cron script của mình để tải trình phân tích cú pháp và chạy trên composer.json
. Việc này áp dụng cụ thể cho các thư mục nhất định của dự án của bạn. Dưới đây là một ví dụ bạn có thể thích nghi vào hệ thống của bạn.
#!/usr/local/bin/php <?php require_once __DIR__ . '/Configuration.php'; $outputDir = '/path/to/your/packages-mirror'; $composerJsonFile = '/path/to/your/projects/composer.json'; $satisConf = '/path/to/your/mirrored-packages.conf'; $satisUpdater = new SatisUpdater(); $conf = $satisUpdater->parseComposerConf(file_get_contents($composerJsonFile)); file_put_contents($satisConf, $conf); system(sprintf('/path/to/satis/bin/satis build %s %s', $satisConf, $outputDir), $retval); exit($retval);
Khiến cho dự án của bạn sử dụng bản sao
Chúng tôi đã bàn về rất nhiều điều trong bài viết này, nhưng đã không đề cập đến cách mà chúng tôi sẽ hướng dẫn dự án của chúng tôi sử dụng bản sao thay vì Internet. Bạn biết đấy, có phải mặc định là packagist.org? Trừ khi chúng ta làm điều này:
"repositories": [ { "type": "composer", "url": "http://your-mirror-server:4680" } ],
Điều đó sẽ làm cho bản sao của bạn trở thành lựa chọn đầu tiên cho composer. Nhưng việc thêm nó vào composer.json
trong dự án của bạn sẽ không vô hiệu hóa quyền truy cập vào packagist.org. Nếu không thể tìm thấy một package trên máy nhân bản, nó sẽ được tải xuống từ Internet. Nếu bạn muốn chặn tính năng này, bạn cũng có thể muốn thêm dòng sau vào phần repositories bên trên:
"packagist": false
Tổng kết
Thế đấy. Bản sao cục bộ với các package tự động thích ứng và cập nhật. Đồng nghiệp của bạn sẽ không bao giờ phải chờ đợi lâu trong khi họ hoặc máy chủ CI cài đặt tất cả các yêu cầu của dự án của bạn. Chúc vui vẻ.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post