Before I tell you more about how I created a Websocket server with PHP to provide communication access between online web apps and local thermal printers, I would like to express my gratitude to donors who have provided support for what I'm doing here.
Donors:
GitHub Sponsors:
Previous story...
Build Warehouse and Store Management System - Pt. 3
Imam Ali Mustofa ・ Feb 18 '22
Hello punk! I am Freestyler, many question a had on internet about how to print from online web app to local printer "thermal"?. Yep! Thermal Printer that usually used in POS Application, sound like whaaaaat.... (just kidding, nothing!)
Sorry... am narcissistic (in my head) person but yeah (am health BTW). So, what is my receipt?
The Receipt (Requirements)
In a long time ago am PHP Developer and then I have backstreet relationship (try to make it word like Hacker) with JavaScript (and falling in love with it) and now I am Playgrammer.
The Framework
Both of them it's just like Documentation.
When it's good, it's very good. When it's bad, it's better than nothing. - Dick Brandon
The Dependencies
- React useWebSocket Hook WebSocket Client to talk with his mama
- ESCPOS-PHP blank paper that client can wrote their homework and share with his mama
- Dexie.js IndexedDB client to store printer setting (optional)
The Devices
- Your Computer / Laptop (nah!)
- Any Thermal Printer that compatible with ESCPOS-PHP Package
Additional Requirement
- Coffee - Required!
- Ciggarettes - Required!
- Music that you hate so much! - Required!
Open The Door
Tok tok tok... open the door please. Open your unusual Text Editor and open terminal window then create your brand new react app! (FYI: I will not tell you how to create react brand new react app, just read their documentation!)
After create brand new react app, then install react-use-websocket
using composer (it's wrong way dude!).
This is just (F) example that I used in my project so don't ask in different way. Just wrote in your way of imagination.
Create Printer Setting Storage
// Filename: src/db/index.js
import Dexie from 'dexie';
export const db = new Dexie('YOUR-STORAGE-NAME');
db.version(1).stores({
printer_settings: '++id, app_name, printer_name, interface, font_type, ch_line_1, ch_line_2, ch_line_3, cf_line_1, cf_line_2, cf_line_3, cl_operator, cl_time, cl_trx_number, cl_costumer_name, cl_discount, cl_total_discount, cl_total, cl_tax, cl_member, cl_paid, cl_return, cl_loan, cl_saving, cl_tempo, line_feed_each_in_items, column_width, more_new_line, pull_cash_drawer, template'
});
Create Printer Setting Page
Create printer setting page to allow user setting their own language and how receipt will look like.
The printer setting page contains field like in Dexie Storage definition.
// Filename: src/constants/index.js
// NOTE:
// - cl = Custom Language
// - ch = Custom Header
// - cf = Custom Footer
export const DEFAULT_PRINTER_SETTING = {
app_name: 'Your Application Name',
printer_name: '/dev/usb/lp0',
interface: 'linux-usb',
font_type: 'C',
ch_line_1: 'Your Header Line 1',
ch_line_2: 'Your Header Line 2',
ch_line_3: 'Your Header Line 3',
cf_line_1: 'Your Footer Line 1',
cf_line_2: 'Your Footer Line 2',
cf_line_3: 'Your Footer Line 3',
cl_operator: 'Operator Name',
cl_time: 'Time',
cl_trx_number: 'Transaction Number',
cl_customer_name: 'Customer Name',
cl_discount: 'Discount',
cl_total_discount: 'Total Discount',
cl_total: 'Total',
cl_tax: 'Tax',
cl_member: 'Member',
cl_paid: 'Paid',
cl_return: 'Return',
cl_debt: 'Debt',
cl_saving: 'Saving',
cl_due_date: 'Due date',
line_feed_each_in_items: 15,
column_width: 40,
more_new_line: 1,
pull_cash_drawer: 'no',
template: 'epson'
}
const interfaceOptions = [
{ value: 'cpus', key: 'CPUS' },
{ value: 'ethernet', key: 'Ethernet' },
{ value: 'linux-usb', key: 'Linux USB' },
{ value: 'smb', key: 'SMB' },
{ value: 'windows-usb', key: 'Windows USB' },
{ value: 'windows-lpt', key: 'Windows LPT' },
]
const fontTypeOptions = [
{ value: 'A', key: 'Font Type A' },
{ value: 'B', key: 'Font Type B' },
{ value: 'C', key: 'Font Type C' },
]
Create form that provide user to input settings. You can create with Formik or something that you know to create form in react.
In my (F) case, I use Formik and make DEFAULT_PRINTER_SETTING
constant as form initialValues
and use interfaceOptions
and fontTypeOptions
to create dropdown options in form.
Create Form Submit Handler
As you can see in above, form need to submit to store setting in Dexie Storage.
// Filename: src/pages/PrinterSetting.js
import { db } from '../db'
import { DEFAULT_PRINTER_SETTING } from '../constants'
const formOnSubmit = async (values) => {
try {
db.printer_settings.count(count => {
if (count > 0) {
db.printer_settings.update(1, values).then(updated => {
if (updated)
console.log("Printer setting was updated!");
else
console.warn("Nothing was updated - there were no update changed");
});
} else {
db.printer_settings.add(DEFAULT_PRINTER_SETTING).then(() => {
console.log("New setting just created using default setting");
}).catch(error => {
console.error(error)
})
}
})
} catch (error) {
console.error(error)
}
}
After creating printer setting page, don't forget to drink your coffee and fire your ciggarettes, play next song that you hate so much in loud volume!
Create POS Context
To make life easy... create POS Context
and Provider
in react. Place it and wrap you app on it.
We need to make WebSocket Client that share it in whole app:
- Share WebSocket Connection Status
- Create Indicator
- Send Message to WebSocket Server
// Filename: src/contexts/POSContext.js
import useWebSocket from 'react-use-websocket'
import { db } from '../db'
import { DEFAULT_PRINTER_SETTING } from '../constants'
const POSContext = createContext()
const SOCKET_URL = 'ws://<your-websocket-address:port>'
const generateAsyncUrlGetter = (url, timeout = 2000) => () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(url);
}, timeout);
});
};
const POSProvider = ({ children }) => {
const [currentSocketUrl, setCurrentSocketUrl] = useState(SOCKET_URL)
const [printerSettings, setprinterSettings] = useState()
const { sendMessage, sendJsonMessage, readyState } = useWebSocket(
currentSocketUrl,
{
share: true,
shouldReconnect: () => true,
reconnectAttempts: 3,
reconnectInterval: 3000,
onError: (event) => {
alert.show('WebSocket trying to connect to sever but failed!', {
type: 'error',
position: 'bottom right',
timeout: 3000
})
},
onOpen: (event) => {
alert.show('WebSocket connection establised!', {
type: 'success',
position: 'bottom right',
timeout: 3000
})
},
onClose: (event) => {
alert.show('WebSocket connection is closed!', {
type: 'warning',
position: 'bottom right',
timeout: 3000
})
},
onReconnectStop: () => 6
}
)
const readyStateString = {
0: 'CONNECTING',
1: 'OPEN',
2: 'CLOSING',
3: 'CLOSED',
}[readyState]
const reconnectWebSocket = useCallback(() => {
if (readyState === 3) {
setCurrentSocketUrl(generateAsyncUrlGetter(SOCKET_URL))
} else {
setCurrentSocketUrl(null)
}
}, [setCurrentSocketUrl, readyState])
const initData = useCallback(async () => {
if (readyState === 1) {
db.printer_settings.count(count => {
if (count > 0) {
setprinterSettings(first(settings))
console.log('Setting is exists')
} else {
setprinterSettings(DEFAULT_PRINTER_SETTING)
console.log('Setting is not exists!')
}
})
} else {
console.log('Setting is not exists!')
setprinterSettings(DEFAULT_PRINTER_SETTING)
}
}, [readyState])
useEffect(() => {
if (readyState === 1) initData()
}, [readyState, initData])
return (
<KasirContext.Provider value={{
sendMessage,
sendJsonMessage,
reconnectWebSocket,
readyStateString,
printerSettings
}}>
{children}
</KasirContext.Provider>
)
}
export { KasirContext, KasirProvider }
Drink coffee and smoke! More loud volume!
Create Indicator Button
This button showing WebSocket connection between Client and Server. This button also can trigger to reconnect to server when connection is closed.
// Filename: src/components/IndicatorButton.js
import React, { useContext } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { POSContext } from '../contexts/POSContext';
function IndicatorButton() {
const posCtx = useContext(POSContext)
return (
<button
className='btn btn-indicator'
onClick={() => posCtx.reconnectWebSocket()}
>
<FontAwesomeIcon icon="fa-solid fa-satellite-dish" />{' '}
Printer Server is:{' '}
<span
className={`${posCtx.readyStateString === 'CONNECTING' ? 'text-info' : (posCtx.readyStateString === 'OPEN') ? 'text-success' : (posCtx.readyStateString === 'CLOSED') ? 'text-secondary' : 'text-danger'}`}
>
{posCtx.readyStateString}
</span>
</button>
)
}
export default IndicatorButton
You can place it that component somewhere in you code.
Now! The WebSocket Client is running but connection has been close for 3 times attempt reconnecting to WebSocket Server.
So what next?!
Create WebSocket Server
Creating websocket server using workerman
is like magic.
- Create new PHP
index.php
file insideprinter-server
directory - Visit Workerman Repository
- Install
workerman
andescpos-php
package insideprinter-server
directory usingnpm
(that's dumb!) - copy and paste A simple websocket example
- go to your bus terminal and run
php index.php start
That's it, nothing else! and you can't do any printing call with that code. It's easy right?
WTF! Are you kidding me?! (That's sound great BTW. Thank you)
Don't mad me! Just Kidding Please!
Now update websocket server script with this code! I promise you... (My head sound: $^U@%G@YT@#.....)
But before you touch websocket server script you need to know what data that printer server needed?
Am Indonesian so don't mad me if my source code contains any Indonesian Language that mixed with English Language. You can translate Indonesia > English using Translator btw or after that you can learn more Indonesian Language (Hahaha just kidding! Next!)
Payload That WebSocket Server (Printer Server Needed)
{
"trx": {
"nama_kasir": "Umam",
"no_transaksi": "nomor-transaksi",
"trx": "only-number-trx",
"nama_pelanggan": "nama-pelanggan",
"tgl_transaksi": "tanggal-transaksi-format-yyyy-mm-dd",
"diskon": "jumlah-diskon",
"total_transaksi": "total-transaksi",
"uang_diterima": "uang-diterima",
"uang_kembali": "uang-kembali",
"jenis_pembayaran": "jenis-pembayaran",
"jatuh_tempo": "tanggal-jatuh-tempo",
"uang_titip": "jumlah-uang-titip",
"ekspedisi": null,
"ongkir": null,
"pajak": "0",
"potongan_member": "0"
},
"text_kembalian": 0,
"belanja": [
{
"barang_nama": "MIE SEDAP SOTO",
"harga": "0",
"jumlah": "50 Dus",
"sub_total": "5125000",
"initial_code": "BRG-605A5B07C3EE5",
"no_transaksi": "nomor-transaksi"
},
{
"barang_nama": "MIE SEDAP GORENG",
"harga": "107000",
"jumlah": "38 Dus",
"sub_total": "4066000",
"initial_code": "BRG-605A5B01530C4",
"no_transaksi": "nomor-transaksi"
},
{
"barang_nama": "PATI SG",
"harga": "0",
"jumlah": "100 Sak",
"sub_total": "25450000",
"initial_code": "BRG-625678D10EC80",
"no_transaksi": "nomor-transaksi"
}
],
"alamat_gudang": "Jl. AHMAD YANI - DESA SAMBIROTO - TAYU - PATI <br>\r\nDepan Masjid Baitussalam <br>\r\n0853 - 2622 - 5333 / 0856 - 4165 - 5202",
"app_name": "CV. Kalimasodo Agriculture",
"nama_lengkap": "Umam",
"from": "posclient",
"command": "posclient",
"printer_name": "EPSON TM-U220 Receipt",
"printer_settings": {
"printer_name": "/dev/usb/lp0",
"interface": "linux-usb",
"font_type": "A",
"custom_print_header": [
"Your Company Name",
"Your Subheading for Receipt",
"Your Third text on header"
],
"custom_print_footer": [
"Your Company Name",
"Your Subheading for Receipt",
"Your Third text on footer"
],
"custom_language": {
"operator": "Operator",
"time": "Time",
"trx_number": "TRX Number",
"costumer_name": "Costumer",
"discount": "Discount",
"total_discount": "Total Disc" ,
"total": "Total",
"tax": "Tax",
"member": "Member",
"paid": "Paid",
"return": "Return",
"loan": "Loan",
"saving": "Saving",
"tempo": "Tempo"
},
"line_feed_each_in_items": 1,
"column_width": 48,
"more_new_line": 1,
"pull_cash_drawer": false
}
}
You can format that request payload as you want with that schema.
Create WebSocket Printer Server
Following is all the code in index.php
<?php
use Workerman\Worker;
use Mike42\Escpos\Printer;
use Mike42\Escpos\PrintConnectors\DummyPrintConnector;
use Mike42\Escpos\PrintConnectors\CupsPrintConnector;
use Mike42\Escpos\PrintConnectors\NetworkPrintConnector;
use Mike42\Escpos\PrintConnectors\FilePrintConnector;
use Mike42\Escpos\PrintConnectors\WindowsPrintConnector;
use Mike42\Escpos\CapabilityProfile;
require_once __DIR__ . '/vendor/autoload.php';
// Create a Websocket server
$ws_worker = new Worker('websocket://0.0.0.0:2112');
// Emitted when new connection come
$ws_worker->onConnect = function ($connection) {
echo "New connection\n";
};
// Emitted when data received
$ws_worker->onMessage = function ($connection, $payload) {
// Send hello $data
$data = json_decode($payload, TRUE);
$options = $data['printer_settings'];
echo '> metadata: ' . $data['from'] . PHP_EOL;
sendingMessage($connection, createMessage('info', 'metadata: ' . $data['from']));
echo '> activer_printer: ' . $data['printer_name'] . PHP_EOL;
sendingMessage($connection, createMessage('info', 'active_printer: ' . $data['printer_name']));
echo '< echo', "\n";
print_r($options);
if ($data['from'] === 'posclient') {
print_reciept($data, $options);
} else if ($data['from'] === 'testprinter') {
}
};
// Emitted when connection closed
$ws_worker->onClose = function ($connection) {
echo "Connection closed\n";
};
// Run worker
Worker::runAll();
function print_reciept($arrData, $options = null)
{
// Printer Connector
$allowed_print_interfaces = ['cpus', 'ethernet', 'linux-usb', 'smb', 'windows-usb', 'windows-lpt'];
if (in_array($options['interface'], $allowed_print_interfaces)) {
switch ($options['interface']) {
case 'cpus':
$connector = new CupsPrintConnector($options['printer_name']);
break;
case 'ethernet':
$connector = new NetworkPrintConnector($options['printer_name'], 9100);
break;
case 'linux-usb':
$connector = new FilePrintConnector($options['printer_name']);
break;
case 'smb':
case 'windows-usb':
case 'windows-lpt':
default:
$connector = new WindowsPrintConnector($options['printer_name']);
break;
}
} else {
// Make sure you load a Star print connector or you may get gibberish.
$connector = new DummyPrintConnector();
$profile = CapabilityProfile::load("TSP600");
}
// Font Type
$selectPrinterFont = [
'A' => Printer::FONT_A,
'B' => Printer::FONT_B,
'C' => Printer::FONT_C
];
if ($options['template'] === 'epson') {
$fontSettings = [
'header_nota' => $selectPrinterFont['B'],
'sub_header_nota' => $selectPrinterFont['B'],
'detail_operator' => $selectPrinterFont['C'],
'detail_balanja' => $selectPrinterFont['C'],
'sub_promo' => $selectPrinterFont['B'],
'footer' => $selectPrinterFont['C'],
'sub_footer' => $selectPrinterFont['B']
];
} else {
$fontSettings = [
'header_nota' => $selectPrinterFont['C'],
'sub_header_nota' => $selectPrinterFont['C'],
'detail_operator' => $selectPrinterFont['C'],
'detail_balanja' => $selectPrinterFont['C'],
'sub_promo' => $selectPrinterFont['C'],
'footer' => $selectPrinterFont['C'],
'sub_footer' => $selectPrinterFont['C']
];
}
echo "Line feed each item: " . $options['line_feed_each_in_items'];
$printer = new Printer($connector);
$column_width = ($options['template'] === 'epson') ? 40 : 48;
$divider = str_repeat("-", $column_width) . "\n";
$optnl = ($options['template'] === 'epson') ? 0 : $options['more_new_line'];
$more_new_line = str_repeat("\n", $optnl);
// Membuat header nota
$printer->initialize();
$printer->selectPrintMode(Printer::MODE_DOUBLE_HEIGHT); // Setting teks menjadi lebih besar
$printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
$printer->setFont($fontSettings['header_nota']);
$printer->setEmphasis(true);
$printer->text($arrData['app_name'] . "\n");
$printer->setEmphasis(false);
$printer->feed(1);
// Membuat sub header nota
$printer->initialize();
$printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
$printer->setFont($fontSettings['sub_header_nota']);
$printer->setEmphasis(true);
if (count($options['custom_print_header']) > 0) {
for ($i = 0; $i < count($options['custom_print_header']); $i++) {
$header_text = str_replace('\r\n', "", $options['custom_print_header'][$i]);
$header_text = str_replace('<br>', PHP_EOL, $header_text);
$printer->text(strtoupper($header_text) . "\n");
}
} else {
$printer->text("MELAYANI SEPENUH HATI\n");
$printer->text(strtoupper(str_replace("<br>", "", $arrData['alamat_gudang'])) . "\n");
$printer->text("Buka Jam 07:30 - 16:30\n");
}
$printer->setEmphasis(false);
$printer->feed(1);
// Membuat detail operator
$printer->initialize();
$printer->setLineSpacing(20);
$printer->setFont($fontSettings['detail_operator']);
$printer->setEmphasis(true);
$printer->text($divider);
$printer->text(buatBarisSejajar($options['custom_language']['operator'] . " : ", $arrData['nama_lengkap'] . $more_new_line, $column_width));
$printer->text(buatBarisSejajar($options['custom_language']['time'] . " : ", $arrData['trx']['tgl_transaksi'] . ' ' . date('H:i:s') . $more_new_line, $column_width));
$printer->text(buatBarisSejajar($options['custom_language']['trx_number'] . " : ", $arrData['trx']['trx'] . $more_new_line, $column_width));
$printer->text(buatBarisSejajar($options['custom_language']['costumer_name'] . " : ", $arrData['trx']['nama_pelanggan'] . $more_new_line, $column_width));
$printer->text($divider);
$printer->setEmphasis(false);
$printer->feed(1);
// Print Detail Belanja
$printer->initialize();
$printer->setLineSpacing(20);
$printer->setFont($fontSettings['detail_balanja']);
$printer->setEmphasis(true);
$no = 1;
foreach ($arrData['belanja'] as $b) {
$printer->text(buatBarisContent($b['barang_nama'], idr_format($b['sub_total'], false), $options['template']));
$printer->text('@ ' . idr_format($b['harga'], true) . " x " . $b['jumlah'] . "\n");
$printer->feed($options['line_feed_each_in_items']);
$no++;
}
$printer->setEmphasis(false);
$printer->feed(1);
if (count($arrData['promo']) > 0) {
// Membuat sub promo
$printer->initialize();
$printer->setLineSpacing(20);
$printer->feed(1);
$printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
$printer->setFont($fontSettings['sub_promo']);
$printer->setEmphasis(true);
$printer->text($divider);
for ($i = 0; $i < count($arrData['promo']); $i++) {
$printer->text(wordwrap($arrData['promo'][$i], 35, "\n") . "\n\n");
}
$printer->setEmphasis(false);
$printer->feed(3);
}
// Print Footer
$printer->initialize();
$printer->setLineSpacing(20);
$printer->setFont($fontSettings['footer']);
$printer->setEmphasis(true);
$printer->text($divider);
// Total Belanja
if ($arrData['trx']['sebelum_diskon'] != $arrData['trx']['total_transaksi']) {
$printer->text(buatBarisSejajar('Total Belanja', idr_format($arrData['trx']['sebelum_diskon']), $column_width) . $more_new_line);
}
// Total Pajak
if ($arrData['trx']['pajak'] != 0) {
$printer->text(buatBarisSejajar($options['custom_language']['tax'], idr_format($arrData['trx']['pajak']), $column_width) . $more_new_line);
}
// Total Diskon
if ($arrData['trx']['diskon'] != 0) {
$printer->text(buatBarisSejajar($options['custom_language']['discount'], idr_format($arrData['trx']['diskon']), $column_width) . $more_new_line);
}
// Total Potongan Member
if ($arrData['trx']['potongan_member'] != 0) {
$printer->text(buatBarisSejajar($options['custom_language']['member'], $arrData['trx']['potongan_member'], $column_width) . $more_new_line);
}
$printer->text(buatBarisSejajar($options['custom_language']['total'], idr_format($arrData['trx']['total_transaksi']), $column_width) . $more_new_line);
// Total Bayar
$bayar = ($arrData['trx']['uang_diterima'] != 0) ? number_format($arrData['trx']['uang_diterima']) : '-';
$printer->text(buatBarisSejajar($options['custom_language']['paid'], $bayar, $column_width) . $more_new_line);
$text_kembalian = ($arrData['trx']['jenis_pembayaran'] != 'Non Tunai') ? number_format($arrData['trx']['uang_kembali']) : '-';
$printer->text(buatBarisSejajar($options['custom_language']['return'], $text_kembalian, $column_width) . $more_new_line);
// End Total Kembalian
if ($arrData['trx']['jenis_pembayaran'] == 'Non Tunai') {
$printer->text(buatBarisSejajar($options['custom_language']['tempo'], date('d/m/Y', strtotime($arrData['trx']['jatuh_tempo'])), $column_width) . $more_new_line);
$printer->text(buatBarisSejajar($options['custom_language']['saving'], number_format($arrData['trx']['uang_titip']), $column_width) . $more_new_line);
$printer->text(buatBarisSejajar($options['custom_language']['loan'], number_format($arrData['trx']['total_transaksi'] - $arrData['trx']['uang_titip']), $column_width) . $more_new_line);
}
$printer->setEmphasis(false);
$printer->feed(1);
// Membuat sub footer nota
$printer->initialize();
// $printer->setLineSpacing(20);
$printer->feed(1);
$printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
$printer->setFont($fontSettings['sub_footer']);
$printer->setEmphasis(true);
if (count($options['custom_print_footer']) > 0) {
$printer->text("Terimakasih sudah berbelanja di\n");
for ($i = 0; $i < count($options['custom_print_footer']); $i++) {
$printer->text($options['custom_print_footer'][$i] . "\n");
}
} else {
$printer->text("Terimakasih sudah berbelanja di\n");
$printer->text("CV. Kalimosodo Angriculture\n");
$printer->text("Barang yang sudah dibeli tidak bisa\n");
$printer->text("dikembalikan.\n");
}
$printer->setEmphasis(false);
$printer->feed(3);
$printer->cut();
// Get the data out as a string
// $data = $connector -> getData();
// echo $data . PHP_EOL;
/* Pulse */
if ($options['pull_cash_drawer']) {
$printer->pulse();
}
// Close the printer when done.
$printer->close();
}
function sendingMessage($connection, $message)
{
$connection->send($message);
}
function createMessage($type = 'info', $message)
{
$timestamps = date('Y/m/d H:i:s');
$log = strtoupper($type);
return "[$timestamps][$log] $message";
}
function buatBarisSejajar($kolom1, $kolom2, $x)
{
$divider = str_repeat("-", $x);
$full_width = strlen($divider);
$half_width = $full_width / 2;
$rawText = str_pad($kolom1, $half_width);
$rawText .= str_pad($kolom2, $half_width, " ", STR_PAD_LEFT);
return $rawText . "\n";
}
function buatBarisContent($kolom1, $kolom2, $template)
{
$paperLength = ($template === 'epson') ? 40 : 48;
$divider = str_repeat("-", $paperLength);
$full_width = strlen($divider);
$half_width = $full_width / 2;
$total_x1 = ($template === 'epson') ? 18 : 22;
$total_x2 = ($template === 'epson') ? 21 : 25;
$lebar_kolom_1 = $total_x1; // asale 18
$lebar_kolom_2 = $total_x2; // asale 21
// Melakukan wordwrap(), jadi jika karakter teks melebihi lebar kolom, ditambahkan \n
$kolom1 = wordwrap($kolom1, $lebar_kolom_1, "\n", true);
$kolom2 = wordwrap($kolom2, $lebar_kolom_2, "\n", true);
// Merubah hasil wordwrap menjadi array, kolom yang memiliki 2 index array berarti memiliki 2 baris (kena wordwrap)
$kolom1Array = explode("\n", $kolom1);
$kolom2Array = explode("\n", $kolom2);
// Mengambil jumlah baris terbanyak dari kolom-kolom untuk dijadikan titik akhir perulangan
$jmlBarisTerbanyak = max(count($kolom1Array), count($kolom2Array));
// Mendeklarasikan variabel untuk menampung kolom yang sudah di edit
$hasilBaris = array();
// Melakukan perulangan setiap baris (yang dibentuk wordwrap), untuk menggabungkan setiap kolom menjadi 1 baris
for ($i = 0; $i < $jmlBarisTerbanyak; $i++) {
// memberikan spasi di setiap cell berdasarkan lebar kolom yang ditentukan,
$hasilKolom1 = str_pad((isset($kolom1Array[$i]) ? $kolom1Array[$i] : ""), $lebar_kolom_1);
$hasilKolom2 = str_pad((isset($kolom2Array[$i]) ? $kolom2Array[$i] : ""), $lebar_kolom_2, " ", STR_PAD_LEFT);
// Menggabungkan kolom tersebut menjadi 1 baris dan ditampung ke variabel hasil (ada 1 spasi disetiap kolom)
$hasilBaris[] = $hasilKolom1 . " " . $hasilKolom2;
}
// Hasil yang berupa array, disatukan kembali menjadi string dan tambahkan \n disetiap barisnya.
return implode("\n", $hasilBaris) . "\n";
}
function idr_format($number, $prefix = false, $decimal_number = 0, $decimal_point = ',', $thousand_point = '.')
{
return ($prefix == false) ? number_format($number, $decimal_number, $decimal_point, $thousand_point) : 'Rp. ' . number_format($number, $decimal_number, $decimal_point, $thousand_point);
}
To run the above code we need to use the following command in the command line, with this command:
➜ php index.php start
Workerman[index.php] start in DEBUG mode
------------------------------------------- WORKERMAN --------------------------------------------
Workerman version:4.1.4 PHP version:7.4.33 Event-Loop:\Workerman\Events\Select
-------------------------------------------- WORKERS ---------------------------------------------
proto user worker listen processes status
tcp darkterminal none websocket://0.0.0.0:2112 1 [OK]
--------------------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.
If you want something more efficient and don't repeat the command above, please use supervisord
, systemd
, or nssm
(NSSM - the Non-Sucking Service Manager for Windows users)
If you find this blog is useful and open your mind to another side of the world of IT and want to give me a cup of coffee or become a sponsor on my GitHub account:
For sponsors/donors, your name will be included in the upcoming article, so please make sure to provide your username from your DEV.to, Twitter, Github, or any other social media accounts you have.
Top comments (0)