Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions classes/Modules/NetworkPrinter/Bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Xentral\Modules\NetworkPrinter;

use ApplicationCore;
use Xentral\Core\DependencyInjection\ContainerInterface;

/**
* Modul-Wrapper fuer die NetworkPrinter-Library unter www/lib/Printer/NetworkPrinter/.
*
* Die eigentliche Printer-Implementation bleibt update-safe dort liegen (kein
* Code-Umzug, kein Namespace-Wechsel), weil sie als OpenXE-Drucker-Plugin von
* pages/drucker.php::loadPrinterModul() per direkter require_once-Kette geladen
* wird und diese Pfade nicht gebrochen werden duerfen. Zusaetzlich ist die
* Library als Pull-Request openxe-org/openxe#257 bei upstream offen, ein
* Code-Umzug wuerde den Merge erschweren.
*
* Dieser Bootstrap stellt die Klasse zusaetzlich als DI-Service zur Verfuegung,
* damit sie aus anderen Modulen (z.B. LexwareOffice oder eine zukuenftige
* Belegdrucker-Integration) per Container-Lookup erreichbar ist, ohne den
* require_once-Pfad an mehreren Stellen zu duplizieren.
*/
final class Bootstrap
{
/**
* Absoluter Pfad zur Library-Hauptklasse im Legacy-Pfad.
*
* Wird in einem evtl. Integration-Branch nach classes/Modules/NetworkPrinter/lib
* verschoben; dieser Bootstrap ist der einzige Ort, der das wissen muss.
*/
private const LIBRARY_ENTRY = __DIR__ . '/../../../www/lib/Printer/NetworkPrinter/NetworkPrinter.php';

/**
* Service-Registry-Map fuer Auto-Discovery.
*
* @return array<string, string>
*/
public static function registerServices(): array
{
return [
'NetworkPrinterFactory' => 'onInitNetworkPrinterFactory',
];
}

/**
* Liefert eine Factory-Closure, die fuer eine gegebene Drucker-ID eine
* NetworkPrinter-Instanz baut. Die Klasse \NetworkPrinter erbt von
* \PrinterBase und erwartet als Constructor-Argumente ($app, $id) — die
* eigentlichen Verbindungsdaten (host, port, protocol, ...) zieht der
* Drucker selbst aus der Tabelle `drucker` per id.
*
* \NetworkPrinter ist global namespaced (kein Xentral\...\NetworkPrinter),
* darum hier root-namespaced referenziert.
*
* @return callable(int): \NetworkPrinter
*/
public static function onInitNetworkPrinterFactory(ContainerInterface $container): callable
{
require_once self::LIBRARY_ENTRY;

/** @var ApplicationCore $app */
$app = $container->get('LegacyApplication');

return static function (int $printerId) use ($app): \NetworkPrinter {
return new \NetworkPrinter($app, $printerId);
};
}
}
125 changes: 125 additions & 0 deletions classes/Modules/NetworkPrinter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# NetworkPrinter Module Wrapper

Duenner OpenXE-Modul-Wrapper fuer die `NetworkPrinter`-Library, die unter
`www/lib/Printer/NetworkPrinter/` als Drucker-Plugin liegt. Dieser Wrapper
fuegt nichts an der Library hinzu — er macht sie lediglich ueber den OpenXE
DI-Container erreichbar, damit andere Module den Drucker per Service-Lookup
holen koennen, ohne den `require_once`-Pfad an mehreren Stellen zu duplizieren.

## Architecture Overview

OpenXE laedt Drucker-Plugins zur Laufzeit ueber
`pages/drucker.php::loadPrinterModul($modul, $id)` per direkter
`require_once`-Kette aus `www/lib/Printer/<Modul>/<Modul>.php`. Die `NetworkPrinter`-Library
folgt diesem Schema: ein globaler Klassenname (`class NetworkPrinter extends
PrinterBase`), kein Composer, kein PSR-4-Namespace. Das ist Absicht — nur so
findet `loadPrinterModul()` die Klasse per Filesystem-Konvention.

Dieser Modul-Wrapper liegt parallel dazu unter
`classes/Modules/NetworkPrinter/`. Er enthaelt keine Geschaeftslogik, sondern
nur einen `Bootstrap.php`, der per OpenXE-Service-Auto-Discovery (`registerServices()`)
die Library laedt und eine Factory-Closure als DI-Service registriert.

### Warum kein Code-Umzug

1. **Update-Sicherheit**: Die Library ist als Pull-Request
[openxe-org/openxe#257](https://github.com/openxe-org/openxe/pull/257)
bei upstream offen. Beim Merge wuerde ein parallel umgezogener Code-Stand
im Fork harte Konflikte erzeugen.
2. **Existierende Aufrufer**: `loadPrinterModul()` erwartet die Klasse in
`www/lib/Printer/<Name>/<Name>.php`. Ein Umzug nach `classes/Modules/...`
wuerde das Plugin fuer den Drucker-Spooler unsichtbar machen.
3. **Globale Klassen**: Die Library nutzt root-namespaced
`class NetworkPrinter extends PrinterBase`. Ein nachtraegliches
`namespace Xentral\...` zu setzen wuerde die Auto-Loading-Konvention
brechen.

Der Wrapper referenziert die Library ausschliesslich per Pfad, nicht per
Symbol-Import. Dadurch ueberlebt er einen upstream-Merge ohne Aenderung.

## File Layout

```
classes/Modules/NetworkPrinter/
Bootstrap.php # DI-Registrierung + Factory-Closure
README.md # diese Datei

www/lib/Printer/NetworkPrinter/ (UNVERAENDERT, Plugin-Pfad)
NetworkPrinter.php # Hauptklasse, extends PrinterBase
PrinterType.php
Protocol.php
Driver/
DriverInterface.php
EscPosDriver.php
IppDriver.php
LprDriver.php
RawDriver.php
Exception/
PrinterException.php
PrinterCommunicationException.php
PrinterConfigException.php
PrinterConnectionException.php
PrinterProtocolException.php
Status/
StatusMonitor.php
Util/
ConnectionTest.php
IppEncoder.php
PdfBatcher.php
```

## Requirements

- PHP 7.4 bis 8.5
- Keine Composer-Dependencies
- Keine zusaetzlichen PHP-Extensions ueber das hinaus, was OpenXE selbst
voraussetzt (`sockets`, `openssl`, optional `snmp` fuer Status-Monitoring)

## Usage

```php
/** @var \Xentral\Core\DependencyInjection\ContainerInterface $container */
$factory = $container->get('NetworkPrinterFactory');

// $printerId = id eines Eintrags in der Tabelle `drucker` mit anbindung='NetworkPrinter'
$printer = $factory($printerId);

// Standard-OpenXE-Plugin-Methode aus PrinterBase / NetworkPrinter
$printer->printDocument($pdfPath, $copies);
```

Die Factory liefert eine `callable(int $printerId): \NetworkPrinter`. Die
eigentliche Verbindungs-Konfiguration (host, port, protocol, lpr_queue,
auth_*) zieht `NetworkPrinter` ueber `PrinterBase::getSettings()` selbst aus
der `drucker`-Tabelle anhand der ID — der Aufrufer muss nichts uebergeben
ausser der ID.

## Installation

1. Branch `feature/network-printer-module` auschecken (oder die Files manuell
nach `classes/Modules/NetworkPrinter/` kopieren).
2. Service-Cache invalidieren, damit OpenXE den neuen Bootstrap erkennt:
```
rm "{WFuserdata}/tmp/{WFdbname}/cache_services.php"
```
(`{WFuserdata}` und `{WFdbname}` aus `www/conf/_config.php`.)
3. Beim naechsten Request baut OpenXE den Service-Container neu auf und
`NetworkPrinterFactory` ist verfuegbar.

Kein Datenbank-Migration-Step, kein Install-Script. Der Drucker-Eintrag in
der `drucker`-Tabelle wird wie bei allen Druckern ueber das OpenXE-Backend
unter Stammdaten -> Drucker -> Anbindung "Netzwerkdrucker (IP)" angelegt.

## Status

Dieser Modul-Wrapper ist update-safe. Die zugrunde liegende Library
(`www/lib/Printer/NetworkPrinter/`) ist bei upstream als Pull-Request
[openxe-org/openxe#257](https://github.com/openxe-org/openxe/pull/257)
offen. Wenn der PR gemerged wird, bleibt der Wrapper kompatibel, weil er nur
auf den Pfad `www/lib/Printer/NetworkPrinter/NetworkPrinter.php` und auf den
globalen Klassennamen `\NetworkPrinter` referenziert — beides bleibt nach
einem upstream-Merge unveraendert.

## License

Siehe OpenXE-Hauptlizenz im Root des Repositories.
38 changes: 38 additions & 0 deletions www/lib/Printer/NetworkPrinter/Driver/DriverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

require_once __DIR__ . '/../Exception/PrinterException.php';
require_once __DIR__ . '/../Exception/PrinterConnectionException.php';
require_once __DIR__ . '/../Exception/PrinterCommunicationException.php';

/**
* Interface fuer alle Netzwerk-Druckertreiber.
*/
interface DriverInterface
{
/**
* Sendet Daten an den Drucker.
*
* @param string $data Dateiinhalt (PDF, ZPL, ESC/POS etc.)
* @param array $options Druckoptionen (anzahl, duplex, color etc.)
*
* @return bool true bei Erfolg
*
* @throws PrinterConnectionException Drucker nicht erreichbar
* @throws PrinterCommunicationException Daten nicht vollstaendig gesendet
*/
public function send(string $data, array $options = []): bool;

/**
* Prueft ob der Drucker erreichbar ist (TCP-Connect-Check).
*
* @return bool
*/
public function isAvailable(): bool;

/**
* Gibt die unterstuetzten Optionen des Treibers zurueck.
*
* @return array z.B. ['duplex' => true, 'color' => true, 'tray' => true]
*/
public function getCapabilities(): array;
}
107 changes: 107 additions & 0 deletions www/lib/Printer/NetworkPrinter/Driver/EscPosDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

require_once __DIR__ . '/DriverInterface.php';
require_once __DIR__ . '/../Exception/PrinterConnectionException.php';
require_once __DIR__ . '/../Exception/PrinterCommunicationException.php';

/**
* ESC/POS Treiber fuer Bondrucker (Thermodrucker).
* Sendet ESC/POS-Byte-Streams direkt per TCP an Port 9100.
* Der ESC/POS-Stream wird von der bestehenden phpprint-Klasse in OpenXE erzeugt.
*/
class EscPosDriver implements DriverInterface
{
/** @var string */
private $host;

/** @var int */
private $port;

/** @var int */
private $timeout;

/**
* @param string $host IP-Adresse oder Hostname
* @param int $port TCP-Port (Default: 9100)
* @param int $timeout Timeout in Sekunden (Default: 30)
*/
public function __construct(string $host, int $port = 9100, int $timeout = 30)
{
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
}

/**
* {@inheritdoc}
*/
public function send(string $data, array $options = []): bool
{
$address = sprintf('tcp://%s:%d', $this->host, $this->port);
$fp = @stream_socket_client($address, $errno, $errstr, 5);

if ($fp === false) {
throw new PrinterConnectionException(
sprintf('Verbindung zu Bondrucker %s fehlgeschlagen: %s (%d)', $address, $errstr, $errno)
);
}

try {
stream_set_timeout($fp, $this->timeout);
$dataLen = strlen($data);
$written = 0;

while ($written < $dataLen) {
$chunk = @fwrite($fp, substr($data, $written));
if ($chunk === false || $chunk === 0) {
$meta = stream_get_meta_data($fp);
if (!empty($meta['timed_out'])) {
throw new PrinterCommunicationException(
sprintf('Timeout beim Senden an Bondrucker %s', $address)
);
}
throw new PrinterCommunicationException(
sprintf('Schreibfehler an Bondrucker %s nach %d/%d Bytes', $address, $written, $dataLen)
);
}
$written += $chunk;
}

fflush($fp);
return true;
} finally {
fclose($fp);
}
}

/**
* {@inheritdoc}
*/
public function isAvailable(): bool
{
$fp = @stream_socket_client(
sprintf('tcp://%s:%d', $this->host, $this->port),
$errno, $errstr, 3
);
if ($fp === false) {
return false;
}
fclose($fp);
return true;
}

/**
* {@inheritdoc}
*/
public function getCapabilities(): array
{
return [
'duplex' => false,
'color' => false,
'tray' => false,
'staple' => false,
'paper_width' => true,
'auto_cut' => true,
];
}
}
Loading