snappy + wkhtmltopdf setup

https://github.com/barryvdh/laravel-snappy

composer require barryvdh/laravel-snappy

// windows
composer require wemersonjanuario/wkhtmltopdf-windows

config/app.php

'PDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'SnappyImage' => Barryvdh\Snappy\Facades\SnappyImage::class,

https://github.com/KnpLabs/snappy#wkhtmltopdf-binary-as-composer-dependencies

composer require h4cc/wkhtmltopdf-amd64 0.12.x
composer require h4cc/wkhtmltoimage-amd64 0.12.x

config/snappy.php

<?php
return array(
'pdf' => array(
'enabled' => true,
'binary' => base_path(env('SNAPPY_WKHTMLTOPDF_PATH', 'vendor/wemersonjanuario/wkhtmltopdf-windows/bin/64bit/wkhtmltopdf')),
'timeout' => false,
'options' => array(),
'env' => array(),
),
'image' => array(
'enabled' => true,
'binary' => base_path(env('SNAPPY_WKHTMLTOIMAGE_PATH', 'vendor/wemersonjanuario/wkhtmltopdf-windows/bin/64bit/wkhtmltoimage')),
'timeout' => false,
'options' => array(),
'env' => array(),
),
);

.env

SNAPPY_WKHTMLTOPDF_PATH="vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64"
SNAPPY_WKHTMLTOIMAGE_PATH="vendor/h4cc/wkhtmltoimage-amd64/bin/wkhtmltoimage-amd64"

Laravel: CentOS 7

PHP 7.2 – gunakan remi 

wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
wget http://rpms.remirepo.net/enterprise/remi-release-7.rpm
rpm -Uvh remi-release-7.rpm epel-release-latest-7.noarch.rpm
yum install yum-utils
yum-config-manager --enable remi-php72
yum update
yum install php php-mbstring php-dom php-gd php-opcache

MariaDB

yum install mariadb-server
sudo systemctl start mariadb
sudo systemctl enable mariadb
sudo mysql_secure_installation

NodeJS & NPM

yum install nodejs
npm install

Composer

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" 
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv composer.phar /usr/bin/composer
composer install

httpd

yum install httpd

Laravel, date & sqlsrv

Kes: Aku gunakan Laravel, database sqlsrv yang diletakkan di dalam Docker kerana gunakan MacOS. Aku perlu dapatkan data daripada medan berjenis date atau datetime eg created_at.

Apabila aku tarik data daripada sistem, jenis format yang dikeluarkan tidak dalam bentuk biasa iaitu, Jan 10 2018 12:00:00:AM. Selalunya apa yang akan dikeluarkan adalah seperti 2018-01-10 atau 2018-01-10 12:00:00. Masalah ini tidak berlaku jika kita buat direct query kepada database. Tetapi hanya apa bila melalui eloquent.

Install pdo_sqlsrv. Untuk itu, aku rujuk kepada laman microsoft sendiri.

Kemudian apabila buat query sekali lagi akan keluar error lain pula. pdo_sqlsrv_db_handle_factory: Unknown exception caught.

Untuk masalah itu aku rujuk kepada komen di dalam github ini. Selepas itu tiada masalah.

Laravel Primary Key As String

Ada ketika primary key kita akan gunakan string bukannya integer maka kita perlu beritahu di dalam model bahawa “laravel, sila casting primary key ini sebagai string” jika tidak secara lalainya adalah integer.

Contoh kes. Primary key kita adalah seperti ini:

338243066987929_338331833652219

Apabila kita menggunakan eloquent untuk mengeluarkan primary key tersebut, key tersebut akan terputus apabila terjumpa karakter bukan nombor dan menjadi seperti ini:

338243066987929

Apa yang kita perlu buat adalah tetapkan di dalam model, casting untuk primary key tersebut adalah string.

protected $casts = [
'id' => 'string',
];

Maka primary key tersebut akan dikeluarkan dengan betul selepas itu.

Ganti Redis:: dengan Cache::

Jika anda mempunyai kemampuan untuk pasang ext-redis kepada PHP anda, maka anda tidak perlu menggunakan Redis:: tetapi cukup menggunakan Cache:: sahaja.

Perubahan yang perlu dibuat adalah tukar cache setting didalam .env daripada file kepada redis.

CACHE_DRIVER=redis

Kemudian ubah kod yang sebelum ini adalah panjang:

// app/Http/Repositories/AnnouncementRepository.php

public static function getActiveAnnouncements($date)
{
	$announcements = Redis::get('announcements:active');

	if ($announcements == null) {
		$announcements = Announcement::where('start_at', '<=', $date)
		->where('end_at', '>=', $date)
		->get();

            // make it expired every 6 hours
		Redis::set('announcements:active', $announcements, 43200);
	} else {
		$announcements = json_decode($announcements);
	}

	return $announcements;
}

Kepada yang lebih pendek:

// app/Http/Repositories/AnnouncementRepository.php

public static function getActiveAnnouncements($date)
{
return Cache::remember('announcements:active', 360, function () use ($date) {
return Announcement::where('start_at', '<=', $date)
->where('end_at', '>=', $date)
->get();
});
}

Kebaikkannya adalah dengan menggunakan Redis:: data yang diberikan adalah bentuk json string tetapi menggunakan Cache:: data yang dipulangkan kepada kita adalah di dalam bentuk collection.

Dua database berlainan

Apa yang anda boleh lakukan jika untuk semua query yang berjenis read perlu menggunakan pangkalan data A, dan untuk semua query yang berjenis write perlu menggunakan pangkalan data B.

Itu adalah soalan yang mana aku jumpa apabila menggunkan MySQL InnoDB Cluster. Apabila terdapat lebih daripada satu database yang mempunyai data yang sama tetapi fungsi yang berlainan, setiap query perlu menggunakan database yang khusus.

Dalam isu InnoDB Cluster, kami telah memasang MySQL Router di dalam server sebagai pembantu dalam menentukan IP pangkalan data yang mana perlu dibuat query. Apa yang telah dibuat adalah dengan membuat hubungan kepada pangkalan data adalah seperti berikut:

// Write
IP: 127.0.0.1
Port: 6666

// Read
IP: 127.0.0.1
Port: 6667

Mujur, di dalam Laravel, ada kaedah yang mudah untuk anda membuat penetapan supaya masalah ini diselesaikan oleh Laravel secara automatik.

// app/config/database.php

'mysql' => [
	'read' => [
		'host' => env('DB_HOST_READ', '127.0.0.1'),
		'port' => env('DB_PORT_READ', '3306'),
	],
	'write' => [
		'host' => env('DB_HOST_WRITE', '127.0.0.1'),
		'port' => env('DB_PORT_WRITE', '3306'),
	],
	'driver' => 'mysql',
	'database' => env('DB_DATABASE', 'forge'),
	'username' => env('DB_USERNAME', 'forge'),
	'password' => env('DB_PASSWORD', ''),
	'unix_socket' => env('DB_SOCKET', ''),
	'charset' => 'utf8mb4',
	'collation' => 'utf8mb4_unicode_ci',
	'prefix' => '',
	'strict' => true,
	'engine' => null,
],

Saya telah tambahkan parameter read dan write dan menyatakan apakah host dan port yang perlu digunakan mengikut keadaan query yang mahu dibuat.

// .env

DB_CONNECTION=mysql
DB_HOST_READ=127.0.0.1
DB_PORT_READ=6667
DB_HOST_WRITE=127.0.0.1
DB_PORT_WRITE=6666
DB_DATABASE=nama_database
DB_USERNAME=root
DB_PASSWORD=

Ubah fail env ikut kesesuaian.

Asas Redis

Redis adalah salah satu daripada data cache yang bagi aku amat suka lebih lagi jika digandingkan bersama Laravel kerana bagi aku redis dan laravel agak senang nak dibuat.

Pertama sekali adalah, pasang redis pada server anda. Kalau tak ada maka boleh rujuk pada kaedah pemasangan redis yang pelbagai. Kemudian untuk Laravel, pasang pula library predis yang akan membantu Laravel berhubung dengan redis.

composer require predis/predis

Kalau tak ada sebarang perubahan secara manual yang korang jalankan, sepatutnya dah boleh guna terus. Untuk kali ini aku letakkan contoh dimana apabila seorang pengguna masuk ke paparan log masuk sesebuah sistem, dia akan dapat melihat;

  1. Form login page
  2. Pengumuman yang masih aktif

Maka secara normalnya, setiap kali pengguna masuk ke paparan log masuk, sistem akan membuat satu query baru kepada database untuk mendapatkan senarai pengumuman yang masih aktif. Dengan kata lain, jika ada 1000 pengguna masuk ke sesebuah paparan yang sama maka 1000 kali query yang sama akan dijalankan kepada pangkalan data. Dan disebabkan datanya tidak berubah dalam jangka masa tersebut, maka sebenarnya itu adalah satu jenis pembaziran sumber. Kita boleh kurangkan masalah ini dengan menggunakan cache, dalam kes ini kita gunakan Redis.

// app/Http/Controllers/Auth/LoginController.php

public function showLoginForm()
{
	$today = date('Y-m-d');
	$announcements = Announcement::where('start_at', '<=', $today)
	->where('end_at', '>=', $today)
	->get();

	return view('auth.login')->with(['announcements' => $announcements]);
}

Kod di atas adalah kod asal yang kita akan ubah dengan cara:

  1. Periksa cache di dalam Redis adakah data tersebut sudah ada
  2. Jika tiada, kita query ke pangkalan data
  3. Kemudian masukkan ke dalam cache Redis
  4. Hantar data ke paparan
// app/Http/Controllers/Auth/LoginController.php

public function showLoginForm()
{
	$today = date('Y-m-d');
	$announcements = Redis::get('announcements:active');

	if ($announcements == null) {
		$announcements = Announcement::where('start_at', '<=', $today)
		->where('end_at', '>=', $today)
		->get();

            // make it expired every 6 hours
		Redis::set('announcements:active', $announcements, 43200);
	} else {
		$announcements = json_decode($announcements);
	}

	return view('auth.login')->with(['announcements' => $announcements]);
}

Maka jika kita lihat dengan menggunakan library seperti Laravel Debugbar, kita akan dapat dapati tiada query baru yang dibuat selepas query pertama dilakukan. Kerana selepas query pertama, query kedua hanya mengambil data yang disimpan secara cache daripada Redis sahaja. Dengan kata lain, jika terdapat 1000 request, kita hanya membuat 1 query sahaja. Penjimatan sebanyak 999 query.

Refactor

Kod di atas tiada masalah, cuma kita boleh cantikkannya lagi dengan mengeluarkan kod yang mencari data pengumuman tersebut dan menukarkannya menjadi fungsi yang khusus untuknya. Perkara ini adalah untuk memudahkan kita menggunakan fungsi yang sama jika perlu sebagai contoh untuk memparkan senarai pengumuman yang sama di dashboard selepas log masuk berjaya.

// app/Http/Repositories/AnnouncementRepository.php

public static function getActiveAnnouncements($date)
{
	$announcements = Redis::get('announcements:active');

	if ($announcements == null) {
		$announcements = Announcement::where('start_at', '<=', $date)
		->where('end_at', '>=', $date)
		->get();

            // make it expired every 6 hours
		Redis::set('announcements:active', $announcements, 43200);
	} else {
		$announcements = json_decode($announcements);
	}

	return $announcements;
}

Saya cipta satu fail baru sebagai repository yang dinamakan sebagai AnnouncementRepository.

// app/Http/Controllers/Auth/LoginController.php

public function showLoginForm()
    {
        $announcements = AnnouncementRepository::getActiveAnnouncements(date('Y-m-d'));

        return view('auth.login')->with(['announcements' => $announcements]);
    }

Kini saya hanya perlu memanggil data pengumuman dengan memanggil fungsi getActiveAnnouncements daripada AnnouncementRepository yang telah dibuat. Sekarang fungsi showLoginForm akan lebih ringkas dan fokus kepada perkara yang mahu dilakukan sahaja.

Notakaki

  • Redis + Laravel = mudah
  • Data di dalam Redis berada di dalam memori. Maka RAM perlu sesuai dengan jumlah data yang disimpan.
  • Redis bukan tempat simpan data yang kekal. Hanya sementara.

Parse error: syntax error, unexpected ‘?’, expecting variable (T_VARIABLE)

Aku buat naik taraf daripada PHP 7.0 kepada PHP 7.2 untuk server demo aku dan selepas buat composer update, aku dapat ralat ini apabila cuma masuk ke salah satu sistem yang menggunakan Laravel 5.5 dengan alasannya dia tak boleh faham apa kegunaan “?” di hadapan variable.

Parse error: syntax error, unexpected ‘?’, expecting variable (T_VARIABLE)

Symfony \ Component \ Debug \ Exception \ FatalThrowableError

PHP version via terminal : PHP 7.2.8-1

PHP version via apache: PHP 7.0.31

Maksudnya apache tak menggunakan PHP versi yang betul. Aku suspek apabila membuat a2enmod php7.2, apache tak mematikan php7.0 secara automatik. maka kene buat manual. a2dismod php7.0 kemudian service restart apache.

Puncanya: Apabila menjalankan arahan composer update, composer akan memeriksa versi PHP di CLI dan apabila menjumpai versi 7.2, semua plugin yang digunakan akan dikemaskini mengikut kemampuan versi PHP tersebut. Tetapi apabila apache menjalankan aplikasi, PHP masih menggunakan versi 7.0. Dan ralat keluar.