├── src ├── Exceptions │ ├── ExceptionInterface.php │ ├── PrintingException.php │ ├── InvalidArgument.php │ ├── InvalidOption.php │ ├── InvalidDriverConfig.php │ ├── UnsupportedDriver.php │ ├── DriverConfigNotFound.php │ ├── PrintNodeApiRequestFailed.php │ ├── InvalidSource.php │ └── PrintTaskFailed.php ├── Api │ ├── Cups │ │ ├── Exceptions │ │ │ ├── RangeOverlap.php │ │ │ ├── UnknownType.php │ │ │ ├── InvalidRequest.php │ │ │ ├── CupsRequestFailed.php │ │ │ └── TypeNotSpecified.php │ │ ├── Enums │ │ │ ├── Orientation.php │ │ │ ├── Side.php │ │ │ ├── JobState.php │ │ │ ├── Version.php │ │ │ ├── PrinterState.php │ │ │ ├── AttributeGroupTag.php │ │ │ ├── ContentType.php │ │ │ ├── OperationAttribute.php │ │ │ ├── TypeTag.php │ │ │ ├── Operation.php │ │ │ └── PrinterStateReason.php │ │ ├── Types │ │ │ ├── Primitive │ │ │ │ ├── OctetString.php │ │ │ │ ├── NoValue.php │ │ │ │ ├── Unknown.php │ │ │ │ ├── Enum.php │ │ │ │ ├── Integer.php │ │ │ │ ├── Boolean.php │ │ │ │ ├── Text.php │ │ │ │ └── Keyword.php │ │ │ ├── Uri.php │ │ │ ├── Charset.php │ │ │ ├── MimeMedia.php │ │ │ ├── NaturalLanguage.php │ │ │ ├── NameWithoutLanguage.php │ │ │ ├── TextWithoutLanguage.php │ │ │ ├── Resolution.php │ │ │ ├── Member.php │ │ │ ├── RangeOfInteger.php │ │ │ ├── Collection.php │ │ │ └── DateTime.php │ │ ├── Attributes │ │ │ ├── JobGroup.php │ │ │ ├── PrinterGroup.php │ │ │ ├── OperationGroup.php │ │ │ └── UnsupportedGroup.php │ │ ├── CupsClientInterface.php │ │ ├── BaseCupsClientInterface.php │ │ ├── CupsClient.php │ │ ├── Service │ │ │ ├── AbstractService.php │ │ │ ├── ServiceFactory.php │ │ │ ├── PrintJobService.php │ │ │ └── PrinterService.php │ │ ├── Type.php │ │ ├── Cups.php │ │ ├── Resources │ │ │ ├── PrintJob.php │ │ │ └── Printer.php │ │ ├── PendingRequest.php │ │ ├── AttributeGroup.php │ │ ├── CupsRequestor.php │ │ ├── Util │ │ │ └── RequestOptions.php │ │ ├── PendingPrintJob.php │ │ └── BaseCupsClient.php │ └── PrintNode │ │ ├── Enums │ │ ├── AuthenticationType.php │ │ └── ContentType.php │ │ ├── Exceptions │ │ ├── AuthenticationFailure.php │ │ ├── PrintNodeApiRequestFailed.php │ │ ├── UnexpectedValue.php │ │ └── RequestOptionsFoundInParams.php │ │ ├── PrintNodeApiResponse.php │ │ ├── BasePrintNodeClientInterface.php │ │ ├── Resources │ │ ├── Support │ │ │ ├── PrintRate.php │ │ │ └── PrinterCapabilities.php │ │ ├── Concerns │ │ │ └── HasDateAttributes.php │ │ ├── ApiOperations │ │ │ ├── Retrieve.php │ │ │ ├── All.php │ │ │ ├── Delete.php │ │ │ └── Request.php │ │ ├── PrintJobState.php │ │ ├── Whoami.php │ │ ├── Computer.php │ │ ├── PrintJob.php │ │ └── Printer.php │ │ ├── Service │ │ ├── WhoamiService.php │ │ ├── AbstractService.php │ │ ├── ServiceFactory.php │ │ ├── PrinterService.php │ │ └── ComputerService.php │ │ ├── PrintNode.php │ │ ├── PrintNodeClient.php │ │ ├── PrintNodeClientInterface.php │ │ ├── PrintNodeApiResource.php │ │ └── Util │ │ ├── Util.php │ │ └── RequestOptions.php ├── Contracts │ ├── Logger.php │ ├── PrintJob.php │ ├── Printer.php │ ├── Driver.php │ └── PrintTask.php ├── PrintingLogger.php ├── Concerns │ └── SerializesToJson.php ├── Util │ └── Set.php ├── Facades │ └── Printing.php ├── PrintingServiceProvider.php ├── Drivers │ ├── PrintNode │ │ ├── Entity │ │ │ ├── PrintJob.php │ │ │ └── Printer.php │ │ ├── PrintNode.php │ │ └── PrintTask.php │ └── Cups │ │ ├── Entity │ │ ├── PrintJob.php │ │ └── Printer.php │ │ ├── Cups.php │ │ └── PrintTask.php ├── Enums │ └── PrintDriver.php ├── PrintTask.php ├── Factory.php ├── Printing.php └── Receipts │ └── ReceiptPrinter.php ├── LICENSE.md ├── composer.json ├── config └── printing.php └── README.md /src/Exceptions/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | value; 12 | } 13 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Uri.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidOption.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/Side.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Exceptions/UnexpectedValue.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/PrintNode/PrintNodeApiResponse.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/TextWithoutLanguage.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/Cups/Attributes/JobGroup.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/DriverConfigNotFound.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/JobState.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Api/Cups/Attributes/UnsupportedGroup.php: -------------------------------------------------------------------------------- 1 | value; 13 | } 14 | -------------------------------------------------------------------------------- /src/Exceptions/PrintNodeApiRequestFailed.php: -------------------------------------------------------------------------------- 1 | value); 17 | 18 | return pack('c', $version[0]) . pack('c', $version[1]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Api/PrintNode/BasePrintNodeClientInterface.php: -------------------------------------------------------------------------------- 1 | request('get', '/whoami', opts: $opts, expectedResource: Whoami::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/PrintingLogger.php: -------------------------------------------------------------------------------- 1 | logger->error($message, $context); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/Concerns/HasDateAttributes.php: -------------------------------------------------------------------------------- 1 | toJson(); 14 | } 15 | 16 | public function jsonSerialize(): mixed 17 | { 18 | return $this->toArray(); 19 | } 20 | 21 | public function toJson(): string 22 | { 23 | return json_encode($this->toArray(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Api/Cups/CupsClientInterface.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | $offset += 2; // Value length 18 | 19 | return [$attrName, new static(null)]; 20 | } 21 | 22 | public function encode(): string 23 | { 24 | return pack('n', 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Primitive/Unknown.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | $offset += 2; // Value length 18 | 19 | return [$attrName, new static(null)]; 20 | } 21 | 22 | public function encode(): string 23 | { 24 | return pack('n', 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/PrinterState.php: -------------------------------------------------------------------------------- 1 | refresh(); 24 | 25 | return $instance; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/PrintTask.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | 18 | $valueLen = (unpack('n', $binary, $offset))[1]; 19 | $offset += 2; 20 | 21 | $value = unpack('N', $binary, $offset)[1]; 22 | $offset += $valueLen; 23 | 24 | return [$attrName, new static($value)]; 25 | } 26 | 27 | public function encode(): string 28 | { 29 | return pack('n', 4) . pack('N', $this->value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Primitive/Integer.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | 18 | $valueLen = (unpack('n', $binary, $offset))[1]; 19 | $offset += 2; 20 | 21 | $value = unpack('N', $binary, $offset)[1]; 22 | $offset += $valueLen; 23 | 24 | return [$attrName, new static($value)]; 25 | } 26 | 27 | public function encode(): string 28 | { 29 | return pack('n', 4) . pack('N', $this->value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Primitive/Boolean.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | 18 | $valueLen = (unpack('n', $binary, $offset))[1]; 19 | $offset += 2; 20 | 21 | $value = (bool) unpack('c', $binary, $offset)[1]; 22 | $offset += $valueLen; 23 | 24 | return [$attrName, new static($value)]; 25 | } 26 | 27 | public function encode(): string 28 | { 29 | return pack('n', 1) . pack('c', (int) $this->value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Primitive/Text.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | 18 | $valueLen = (unpack('n', $binary, $offset))[1]; 19 | $offset += 2; 20 | 21 | $value = unpack('a' . $valueLen, $binary, $offset)[1]; 22 | $offset += $valueLen; 23 | 24 | return [$attrName, new static($value)]; 25 | } 26 | 27 | public function encode(): string 28 | { 29 | return pack('n', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Primitive/Keyword.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | public static function fromBinary(string $binary, int &$offset): array 15 | { 16 | $attrName = self::nameFromBinary($binary, $offset); 17 | 18 | $valueLen = (unpack('n', $binary, $offset))[1]; 19 | $offset += 2; 20 | 21 | $value = unpack('a' . $valueLen, $binary, $offset)[1]; 22 | $offset += $valueLen; 23 | 24 | return [$attrName, new static($value)]; 25 | } 26 | 27 | public function encode(): string 28 | { 29 | return pack('n', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Cups/CupsClient.php: -------------------------------------------------------------------------------- 1 | getService($name); 22 | } 23 | 24 | public function getService(string $name): ?Service\AbstractService 25 | { 26 | if ($this->serviceFactory === null) { 27 | $this->serviceFactory = new ServiceFactory($this); 28 | } 29 | 30 | return $this->serviceFactory->getService($name); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/AttributeGroupTag.php: -------------------------------------------------------------------------------- 1 | value => JobGroup::class, 25 | self::OperationAttributes->value => OperationGroup::class, 26 | self::PrinterAttributes->value => PrinterGroup::class, 27 | self::UnSupportedAttributes->value => UnsupportedGroup::class, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Api/Cups/Service/AbstractService.php: -------------------------------------------------------------------------------- 1 | client; 27 | } 28 | 29 | protected function request( 30 | PendingRequest $pendingRequest, 31 | array|null|RequestOptions $opts = [], 32 | ): CupsResponse { 33 | return $this->getClient()->request( 34 | binary: $pendingRequest->encode(), 35 | opts: $opts ?? [], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exceptions/PrintTaskFailed.php: -------------------------------------------------------------------------------- 1 | getService($name); 24 | } 25 | 26 | public function getService(string $name): ?Service\AbstractService 27 | { 28 | if ($this->serviceFactory === null) { 29 | $this->serviceFactory = new ServiceFactory($this); 30 | } 31 | 32 | return $this->serviceFactory->getService($name); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Randall Wilk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Util/Set.php: -------------------------------------------------------------------------------- 1 | $members 20 | */ 21 | public function __construct(array $members = []) 22 | { 23 | foreach ($members as $item) { 24 | $this->_elements[$item] = true; 25 | } 26 | } 27 | 28 | public function includes(string $element): bool 29 | { 30 | return isset($this->_elements[$element]); 31 | } 32 | 33 | public function add(string $element): void 34 | { 35 | $this->_elements[$element] = true; 36 | } 37 | 38 | public function discard(string $element): void 39 | { 40 | unset($this->_elements[$element]); 41 | } 42 | 43 | public function toArray(): array 44 | { 45 | return array_keys($this->_elements); 46 | } 47 | 48 | public function getIterator(): Traversable 49 | { 50 | return new ArrayIterator($this->toArray()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Resolution.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | private static array $unitMap = [ 15 | 3 => 'dpi', 16 | 4 => 'dpc', 17 | ]; 18 | 19 | public static function fromBinary(string $binary, int &$offset): array 20 | { 21 | $attrName = self::nameFromBinary($binary, $offset); 22 | 23 | $valueLen = (unpack('n', $binary, $offset))[1]; 24 | $offset += 2; 25 | 26 | $value = unpack('Np/Np2/cu', $binary, $offset); 27 | $offset += $valueLen; 28 | 29 | return [$attrName, new static($value['p'] . 'x' . $value['p2'] . static::$unitMap[$value['u']])]; 30 | } 31 | 32 | public function encode(): string 33 | { 34 | preg_match('/(\d+)x(\d+)(.*)/', $this->value, $matches); 35 | $reverseMap = array_flip(static::$unitMap); 36 | 37 | return pack('n', 9) . pack('N', $matches[1]) 38 | . pack('N', $matches[2]) 39 | . pack('c', $reverseMap[$matches[3]]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Api/Cups/Type.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | abstract public static function fromBinary(string $binary, int &$offset): array; 23 | 24 | /** 25 | * Returns value length and value in binary 26 | */ 27 | abstract public function encode(): string; 28 | 29 | public function getTag(): int 30 | { 31 | return $this->tag; 32 | } 33 | 34 | public function jsonSerialize(): mixed 35 | { 36 | return $this->value; 37 | } 38 | 39 | /** 40 | * Returns name from binary and increments offset 41 | * 42 | * @return string attribute name 43 | */ 44 | protected static function nameFromBinary(string $binary, int &$offset): string 45 | { 46 | $nameLen = (unpack('n', $binary, $offset))[1]; 47 | $offset += 2; 48 | 49 | $attrName = unpack('a' . $nameLen, $binary, $offset)[1]; 50 | $offset += $nameLen; 51 | 52 | return $attrName; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Member.php: -------------------------------------------------------------------------------- 1 | value; 13 | 14 | /** 15 | * @see https://datatracker.ietf.org/doc/html/rfc3382#section-7.2 16 | */ 17 | public static function fromBinary(string $binary, int &$offset): array 18 | { 19 | // Name is empty 20 | self::nameFromBinary($binary, $offset); 21 | 22 | $valueLen = (unpack('n', $binary, $offset))[1]; 23 | $offset += 2; 24 | 25 | // This will be the attribute name 26 | $value = unpack('a' . $valueLen, $binary, $offset)[1]; 27 | $offset += $valueLen; 28 | 29 | $nextTag = (unpack('ctag', $binary, $offset))['tag']; 30 | $offset++; 31 | 32 | $type = TypeTag::tryFrom($nextTag); 33 | $typeClass = $type->getClass(); 34 | 35 | // This will be the value 36 | $value2 = $typeClass::fromBinary($binary, $offset)[1]; 37 | 38 | return [$value, new static($value2)]; 39 | } 40 | 41 | public function encode(): string 42 | { 43 | $binary = pack('c', $this->value->getTag()); 44 | $binary .= pack('n', 0); // Name length is 0 45 | $binary .= $this->value->encode(); 46 | 47 | return $binary; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/ApiOperations/Delete.php: -------------------------------------------------------------------------------- 1 | instanceUrl(); 22 | 23 | [$response, $opts] = $this->_request('delete', $url, $params, $opts); 24 | 25 | // PrintNode sends an array of IDs that were affected in most DELETE requests. 26 | // If we don't receive the ID, something went wrong. 27 | throw_unless( 28 | is_array($response), 29 | PrintNodeApiRequestFailed::class, 30 | 'Unexpected response received from PrintNode.', 31 | ); 32 | 33 | throw_unless( 34 | in_array($this['id'], $response, true), 35 | PrintNodeApiRequestFailed::class, 36 | 'Resource deletion failed.', 37 | ); 38 | 39 | $this->refreshFrom($this->toArray(), $opts); 40 | 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Facades/Printing.php: -------------------------------------------------------------------------------- 1 | parseDate($this->createTimestamp); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/ContentType.php: -------------------------------------------------------------------------------- 1 | client; 26 | } 27 | 28 | protected function request( 29 | string $method, 30 | string $path, 31 | ?array $params = [], 32 | null|array|RequestOptions $opts = [], 33 | ?string $expectedResource = null, 34 | ) { 35 | return $this->getClient()->request($method, $path, $params ?? [], $opts ?? [], $expectedResource); 36 | } 37 | 38 | protected function requestCollection( 39 | string $method, 40 | string $path, 41 | ?array $params = [], 42 | null|array|RequestOptions $opts = [], 43 | ?string $expectedResource = null, 44 | ): Collection { 45 | return $this->getClient()->requestCollection($method, $path, $params ?? [], $opts ?? [], $expectedResource); 46 | } 47 | 48 | protected function buildPath(string $basePath, int ...$ids): string 49 | { 50 | $ids = implode(',', array_map('urlencode', $ids)); 51 | 52 | return sprintf($basePath, $ids); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/RangeOfInteger.php: -------------------------------------------------------------------------------- 1 | value; 14 | 15 | public static function fromBinary(string $binary, int &$offset): array 16 | { 17 | $attrName = self::nameFromBinary($binary, $offset); 18 | 19 | $valueLen = (unpack('n', $binary, $offset))[1]; 20 | $offset += 2; 21 | 22 | $value = unpack('Nl/Nu', $binary, $offset); 23 | $offset += $valueLen; 24 | 25 | return [$attrName, new static([$value['l'], $value['u']])]; 26 | } 27 | 28 | /** 29 | * Sorts and checks the array for overlaps 30 | * 31 | * @param array $values 32 | * 33 | * @throws RangeOverlap 34 | */ 35 | public static function checkOverlaps(array &$values): bool 36 | { 37 | usort( 38 | $values, 39 | static function ($a, $b) { 40 | return $a->value[0] - $b->value[0]; 41 | } 42 | ); 43 | 44 | $count = count($values); 45 | for ($i = 0; $i < $count - 1; $i++) { 46 | if ($values[$i]->value[1] >= $values[$i + 1]->value[0]) { 47 | throw new RangeOverlap('Range overlap is not allowed!'); 48 | } 49 | } 50 | 51 | return true; // No overlaps found 52 | } 53 | 54 | public function encode(): string 55 | { 56 | return pack('n', 8) . pack('N', $this->value[0]) . pack('N', $this->value[1]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Api/Cups/Service/ServiceFactory.php: -------------------------------------------------------------------------------- 1 | PrinterService::class, 25 | 'printJobs' => PrintJobService::class, 26 | ]; 27 | 28 | public function __construct(protected CupsClientInterface $client) 29 | { 30 | } 31 | 32 | public function __get(string $name): ?AbstractService 33 | { 34 | return $this->getService($name); 35 | } 36 | 37 | public function getService(string $name): ?AbstractService 38 | { 39 | $serviceClass = $this->getServiceClass($name); 40 | if ($serviceClass !== null) { 41 | if (! array_key_exists($name, $this->services)) { 42 | $this->services[$name] = new $serviceClass($this->client); 43 | } 44 | 45 | return $this->services[$name]; 46 | } 47 | 48 | trigger_error('Undefined property ' . static::class . '::$' . $name); 49 | 50 | return null; 51 | } 52 | 53 | protected function getServiceClass(string $name): ?string 54 | { 55 | return self::$classMap[$name] ?? null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Api/PrintNode/PrintNodeClientInterface.php: -------------------------------------------------------------------------------- 1 | $expectedResource the object we should map the response into 19 | */ 20 | public function request( 21 | string $method, 22 | string $path, 23 | array $params = [], 24 | array|RequestOptions $opts = [], 25 | ?string $expectedResource = null, 26 | ); 27 | 28 | /** 29 | * Sends a request to PrintNode's API for a collection of resources. 30 | * 31 | * @param string $method the HTTP method 'delete'|'get'|'post' 32 | * @param string $path the path of the request 33 | * @param array $params the parameters of the request 34 | * @param array|RequestOptions $opts the special modifiers of the request 35 | * @param null|class-string<\Rawilk\Printing\Api\PrintNode\PrintNodeObject> $expectedResource the object we should map each resource into 36 | */ 37 | public function requestCollection( 38 | string $method, 39 | string $path, 40 | array $params = [], 41 | array|RequestOptions $opts = [], 42 | ?string $expectedResource = null, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/PrintingServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-printing') 18 | ->hasConfigFile(); 19 | } 20 | 21 | public function packageRegistered(): void 22 | { 23 | $this->app->singleton( 24 | Factory::class, 25 | fn ($app) => new Factory($app['config']['printing']) 26 | ); 27 | 28 | $this->app->singleton(Driver::class, fn ($app) => $app[Factory::class]->driver()); 29 | 30 | $this->app->singleton( 31 | Printing::class, 32 | fn ($app) => new Printing($app[Driver::class], $app['config']['printing.default_printer_id']) 33 | ); 34 | 35 | $this->bindLogger(); 36 | } 37 | 38 | public function packageBooted(): void 39 | { 40 | $this->registerLogger(); 41 | } 42 | 43 | public function provides(): array 44 | { 45 | return [ 46 | Factory::class, 47 | Driver::class, 48 | Printing::class, 49 | ]; 50 | } 51 | 52 | private function bindLogger(): void 53 | { 54 | $this->app->bind( 55 | Logger::class, 56 | fn ($app) => new PrintingLogger($app->make('log')->channel(config('printing.logger'))), 57 | ); 58 | } 59 | 60 | private function registerLogger(): void 61 | { 62 | if (config('printing.logger')) { 63 | Printing::setLogger($this->app->make(Logger::class)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Drivers/PrintNode/Entity/PrintJob.php: -------------------------------------------------------------------------------- 1 | printer) { 23 | $this->printer = new Printer($job->printer); 24 | } 25 | } 26 | 27 | public function job(): PrintNodePrintJob 28 | { 29 | return $this->job; 30 | } 31 | 32 | public function date(): ?CarbonInterface 33 | { 34 | return $this->job->createdAt(); 35 | } 36 | 37 | public function id(): int 38 | { 39 | return $this->job->id; 40 | } 41 | 42 | public function name(): ?string 43 | { 44 | return $this->job->title; 45 | } 46 | 47 | public function printerId(): int|string 48 | { 49 | return $this->job->printer?->id; 50 | } 51 | 52 | public function printerName(): ?string 53 | { 54 | return $this->job->printer?->name; 55 | } 56 | 57 | public function state(): ?string 58 | { 59 | return $this->job->state; 60 | } 61 | 62 | public function toArray(): array 63 | { 64 | return [ 65 | 'id' => $this->id(), 66 | 'date' => $this->date(), 67 | 'name' => $this->name(), 68 | 'printerId' => $this->printerId(), 69 | 'printerName' => $this->printerName(), 70 | 'state' => $this->state(), 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Api/Cups/Service/PrintJobService.php: -------------------------------------------------------------------------------- 1 | toPendingRequest() 27 | : $pendingJob; 28 | 29 | $response = $this->request($pendingRequest, $opts); 30 | 31 | return $response->jobs()->first(); 32 | } 33 | 34 | public function retrieve(string $uri, array $params = [], array|null|RequestOptions $opts = null): ?PrintJob 35 | { 36 | $pendingRequest = (new PendingRequest) 37 | ->setVersion(Version::V2_1) 38 | ->setOperation(Operation::GetJobAttributes) 39 | ->addOperationAttributes([ 40 | OperationAttribute::JobUri->value => OperationAttribute::JobUri->toType($uri), 41 | OperationAttribute::RequestedAttributes->value => $params[OperationAttribute::RequestedAttributes->value] ?? PrintJob::defaultRequestedAttributes(), 42 | 43 | ...Arr::except($params, OperationAttribute::RequestedAttributes->value), 44 | ]); 45 | 46 | $response = $this->request($pendingRequest, $opts); 47 | 48 | return $response->jobs()->first(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Drivers/Cups/Entity/PrintJob.php: -------------------------------------------------------------------------------- 1 | job->__debugInfo(); 26 | } 27 | 28 | public function job(): CupsPrintJob 29 | { 30 | return $this->job; 31 | } 32 | 33 | public function date(): ?CarbonInterface 34 | { 35 | $date = $this->job->dateTimeAtCreation; 36 | 37 | return filled($date) ? Date::parse($date) : null; 38 | } 39 | 40 | public function id(): string 41 | { 42 | return $this->job->uri; 43 | } 44 | 45 | public function name(): ?string 46 | { 47 | return $this->job->jobName; 48 | } 49 | 50 | public function printerId(): string 51 | { 52 | return $this->job->jobPrinterUri; 53 | } 54 | 55 | public function printerName(): ?string 56 | { 57 | return $this->job->printerName(); 58 | } 59 | 60 | public function state(): ?string 61 | { 62 | return strtolower($this->job->state()?->name); 63 | } 64 | 65 | public function toArray(): array 66 | { 67 | return [ 68 | 'id' => $this->id(), 69 | 'date' => $this->date(), 70 | 'name' => $this->name(), 71 | 'printerId' => $this->printerId(), 72 | 'printerName' => $this->printerName(), 73 | 'state' => $this->state(), 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Enums/PrintDriver.php: -------------------------------------------------------------------------------- 1 | value) . 'Config'; 20 | 21 | $this->{$method}($config); 22 | } 23 | 24 | protected function validatePrintnodeConfig(array $config): void 25 | { 26 | $key = data_get($config, 'key'); 27 | 28 | // We'll attempt to fall back on the static PrintNode::$apiKey value later. 29 | if ($key === null) { 30 | return; 31 | } 32 | 33 | throw_if( 34 | blank($key), 35 | InvalidDriverConfig::invalid('You must provide an api key for the PrintNode driver.'), 36 | ); 37 | } 38 | 39 | protected function validateCupsConfig(array $config): void 40 | { 41 | $ip = data_get($config, 'ip'); 42 | throw_if( 43 | $ip !== null && blank($ip), 44 | InvalidDriverConfig::invalid('An IP address is required for the CUPS driver.'), 45 | ); 46 | 47 | $secure = data_get($config, 'secure'); 48 | throw_if( 49 | $secure !== null && (! is_bool($secure)), 50 | InvalidDriverConfig::invalid('A boolean value must be provided for the secure option for the CUPS driver.'), 51 | ); 52 | 53 | $port = data_get($config, 'port'); 54 | throw_if( 55 | $port !== null && blank($port), 56 | InvalidDriverConfig::invalid('A port must be provided for the CUPS driver.'), 57 | ); 58 | 59 | throw_if( 60 | $port !== null && 61 | ((! is_int($port)) || $port < 1), 62 | InvalidDriverConfig::invalid('A valid port number was not provided for the CUPS driver.'), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Service/ServiceFactory.php: -------------------------------------------------------------------------------- 1 | ComputerService::class, 27 | 'printers' => PrinterService::class, 28 | 'printJobs' => PrintJobService::class, 29 | 'whoami' => WhoamiService::class, 30 | ]; 31 | 32 | public function __construct(protected PrintNodeClientInterface $client) 33 | { 34 | } 35 | 36 | public function __get(string $name): ?AbstractService 37 | { 38 | return $this->getService($name); 39 | } 40 | 41 | public function getService(string $name): ?AbstractService 42 | { 43 | $serviceClass = $this->getServiceClass($name); 44 | if ($serviceClass !== null) { 45 | if (! array_key_exists($name, $this->services)) { 46 | $this->services[$name] = new $serviceClass($this->client); 47 | } 48 | 49 | return $this->services[$name]; 50 | } 51 | 52 | trigger_error('Undefined property: ' . static::class . '::$' . $name); 53 | 54 | return null; 55 | } 56 | 57 | protected function getServiceClass(string $name): ?string 58 | { 59 | return self::$classMap[$name] ?? null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/Collection.php: -------------------------------------------------------------------------------- 1 | value; 16 | 17 | // Collection has an end tag 18 | protected int $endTag = TypeTag::CollectionEnd->value; 19 | 20 | public static function fromBinary(string $binary, int &$offset): array 21 | { 22 | $attrName = self::nameFromBinary($binary, $offset); 23 | $offset += 2; // Value length 24 | 25 | $members = []; 26 | while (unpack('ctag', $binary, $offset)['tag'] === TypeTag::Member->value) { 27 | $nextTag = (unpack('ctag', $binary, $offset))['tag']; 28 | $offset++; 29 | 30 | $type = TypeTag::tryFrom($nextTag); 31 | $typeClass = $type->getClass(); 32 | 33 | [$name, $value] = $typeClass::fromBinary($binary, $offset); 34 | $members[$name] = $value; 35 | } 36 | 37 | // Collection end tags 38 | $offset++; // 0x37 39 | $offset += 4; // Name, value length 40 | 41 | return [$attrName, new static($members)]; 42 | } 43 | 44 | public function encode(): string 45 | { 46 | $binary = pack('n', 0); // Value length is 0 47 | 48 | foreach ($this->value as $key => $value) { 49 | $binary .= pack('c', TypeTag::Member->value); 50 | $binary .= pack('n', 0); // Member name length is 0 51 | 52 | $binary .= pack('n', strlen($key)); 53 | $binary .= pack('a' . strlen($key), $key); 54 | 55 | $binary .= $value->encode(); 56 | } 57 | 58 | // Collection has an end tag (with name, value) 59 | $binary .= pack('c', $this->endTag); 60 | $binary .= pack('n', 0); // End tag name length is 0 61 | $binary .= pack('n', 0); // End tag value length is 0 62 | 63 | return $binary; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/Whoami.php: -------------------------------------------------------------------------------- 1 | Upgrade account link 18 | * @property null|string $creatorEmail The email address of the account that created this sub-account 19 | * @property null|string $creatorRef The creation reference set when the account was created 20 | * @property array $childAccounts Any child accounts present on this account 21 | * @property int|null $credits The number of print credits remaining on this account 22 | * @property int $numComputers The number of computers active on this account 23 | * @property int $totalPrints Total number of prints made on this account 24 | * @property array $versions A collection of versions set on this account 25 | * @property array $connected A collection of computer IDs signed in on this account 26 | * @property array $Tags A collection of tags set on this account 27 | * @property array $ApiKeys A collection of all the api keys set on this account 28 | * @property string $state The status of the account 29 | * @property array $permissions The permissions set on this account 30 | */ 31 | class Whoami extends PrintNodeApiResource 32 | { 33 | public static function classUrl(): string 34 | { 35 | return '/whoami'; 36 | } 37 | 38 | public static function resourceUrl(?int $id = null): string 39 | { 40 | return static::classUrl(); 41 | } 42 | 43 | /** 44 | * Indicates if the account is considered active. 45 | */ 46 | public function isActive(): bool 47 | { 48 | return $this->_values['state'] === 'active'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/OperationAttribute.php: -------------------------------------------------------------------------------- 1 | value); 38 | } 39 | 40 | public function toType(mixed $value = null): Type 41 | { 42 | return match ($this) { 43 | self::PrinterUri, self::JobUri => new Uri($value), 44 | self::DocumentFormat => new MimeMedia($value), 45 | self::JobName => new NameWithoutLanguage($value), 46 | self::WhichJobs => new Keyword($value ?? 'not-completed'), 47 | self::OrientationRequested => new Enum($value ?? Orientation::Portrait->value), 48 | self::Copies => new Integer($value), 49 | self::PageRanges => new RangeOfInteger($value), 50 | self::RequestingUserName => new NameWithoutLanguage(iconv('UTF-8', 'ASCII//TRANSLIT', $value)), 51 | self::Sides => new Keyword($value), 52 | default => null, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Api/Cups/Cups.php: -------------------------------------------------------------------------------- 1 | toKeyword(), 27 | OperationAttribute::JobState->toKeyword(), 28 | OperationAttribute::NumberOfDocuments->toKeyword(), 29 | OperationAttribute::JobName->toKeyword(), 30 | OperationAttribute::DocumentFormat->toKeyword(), 31 | OperationAttribute::DateTimeAtCreation->toKeyword(), 32 | OperationAttribute::JobPrinterStateMessage->toKeyword(), 33 | OperationAttribute::JobPrinterUri->toKeyword(), 34 | ]; 35 | } 36 | 37 | public function state(): ?JobState 38 | { 39 | return JobState::tryFrom($this->jobState); 40 | } 41 | 42 | public function printerName(): ?string 43 | { 44 | // Attempt to extract the printer's name from the uri. 45 | if (preg_match('/printers\/(.*)$/', $this->jobPrinterUri, $matches)) { 46 | return $matches[1]; 47 | } 48 | 49 | return null; 50 | } 51 | 52 | protected function mutateAttributes(array $values): array 53 | { 54 | $values['job-uri'] = $this->attributeValue($values, 'job-uri'); 55 | $values['job-name'] = $this->attributeValue($values, 'job-name'); 56 | $values['job-printer-uri'] = $this->attributeValue($values, 'job-printer-uri'); 57 | $values['job-state'] = $this->attributeValue($values, 'job-state', JobState::Pending->value); 58 | 59 | return $values; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Api/Cups/Types/DateTime.php: -------------------------------------------------------------------------------- 1 | value; 14 | 15 | public static function fromBinary(string $binary, int &$offset): array 16 | { 17 | $attrName = self::nameFromBinary($binary, $offset); 18 | 19 | $valueLen = (unpack('n', $binary, $offset))[1]; 20 | $offset += 2; 21 | 22 | $data = unpack('nY/cm/cd/cH/ci/cs/cfff/aUTCSym/cUTCm/cUTCs', $binary, $offset); 23 | $offset += $valueLen; 24 | 25 | $value = Date::createFromFormat( 26 | 'YmdHisO', 27 | $data['Y'] 28 | . str_pad((string) $data['m'], 2, '0', STR_PAD_LEFT) 29 | . str_pad((string) $data['d'], 2, '0', STR_PAD_LEFT) 30 | . str_pad((string) $data['H'], 2, '0', STR_PAD_LEFT) 31 | . str_pad((string) $data['i'], 2, '0', STR_PAD_LEFT) 32 | . str_pad((string) $data['s'], 2, '0', STR_PAD_LEFT) 33 | . $data['UTCSym'] 34 | . str_pad((string) $data['UTCm'], 2, '0', STR_PAD_LEFT) 35 | . str_pad((string) $data['UTCs'], 2, '0', STR_PAD_LEFT) 36 | ); 37 | 38 | return [$attrName, new static($value)]; 39 | } 40 | 41 | public function encode(): string 42 | { 43 | preg_match('/([+-])(\d{2}):(\d{2})/', $this->value->getOffsetString(), $matches); 44 | 45 | return pack('n', 11) . pack('n', $this->value->format('Y')) 46 | . pack('c', $this->value->format('m')) 47 | . pack('c', $this->value->format('d')) 48 | . pack('c', $this->value->format('H')) 49 | . pack('c', $this->value->format('i')) 50 | . pack('c', $this->value->format('s')) 51 | . pack('c', 0) 52 | . pack('a', $matches[1]) 53 | . pack('c', self::unpad($matches[2])) 54 | . pack('c', self::unpad($matches[3])); 55 | } 56 | 57 | private static function unpad(string $str): string 58 | { 59 | $unpaddedStr = ltrim($str, '0'); 60 | if ($unpaddedStr === '') { 61 | $unpaddedStr = '0'; // Ensure "00" becomes "0" 62 | } 63 | 64 | return $unpaddedStr; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rawilk/laravel-printing", 3 | "description": "Direct printing for Laravel apps", 4 | "keywords": [ 5 | "rawilk", 6 | "laravel-printing", 7 | "PrintNode", 8 | "CUPS", 9 | "ipp", 10 | "Receipt printing", 11 | "Direct printing", 12 | "Raw printing" 13 | ], 14 | "homepage": "https://github.com/rawilk/laravel-printing", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Randall Wilk", 19 | "email": "randall@randallwilk.dev", 20 | "homepage": "https://randallwilk.dev", 21 | "role": "Developer" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.2", 26 | "guzzlehttp/guzzle": "^7.5", 27 | "illuminate/support": "^10.0|^11.0|^12.0", 28 | "mike42/escpos-php": "^4.0", 29 | "spatie/laravel-package-tools": "^1.2|^1.13" 30 | }, 31 | "require-dev": { 32 | "laravel/pint": "^1.5", 33 | "mockery/mockery": ">=1.4", 34 | "orchestra/testbench": "^8.0|^9.0|^10.0", 35 | "pestphp/pest": "^2.34|^3.7", 36 | "pestphp/pest-plugin-laravel": "^2.2|^3.1", 37 | "php-http/message-factory": "^1.1", 38 | "php-http/socket-client": "^2.1", 39 | "psr/http-client": "^1.0", 40 | "psr/http-message": "1.*|^2.0", 41 | "spatie/invade": "^2.1", 42 | "spatie/laravel-ray": "^1.0|^1.29" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Rawilk\\Printing\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Rawilk\\Printing\\Tests\\": "tests" 52 | } 53 | }, 54 | "scripts": { 55 | "post-autoload-dump": [ 56 | "@php ./vendor/bin/testbench package:discover --ansi" 57 | ], 58 | "test": "vendor/bin/pest -p", 59 | "format": "vendor/bin/pint --dirty" 60 | }, 61 | "config": { 62 | "sort-packages": true, 63 | "allow-plugins": { 64 | "pestphp/pest-plugin": true 65 | } 66 | }, 67 | "extra": { 68 | "laravel": { 69 | "providers": [ 70 | "Rawilk\\Printing\\PrintingServiceProvider" 71 | ], 72 | "aliases": { 73 | "Printing": "Rawilk\\Printing\\Facades\\Printing" 74 | } 75 | } 76 | }, 77 | "minimum-stability": "dev", 78 | "prefer-stable": true 79 | } 80 | -------------------------------------------------------------------------------- /src/Api/PrintNode/PrintNodeApiResource.php: -------------------------------------------------------------------------------- 1 | lower() 22 | ->append('s') 23 | ->prepend('/') 24 | ->toString(); 25 | } 26 | 27 | public static function resourceUrl(?int $id = null): string 28 | { 29 | if ($id === null) { 30 | $class = static::class; 31 | 32 | throw new UnexpectedValue( 33 | 'Could not determine which URL to request: ' . 34 | "{$class} instance has invalid ID: {$id}", 35 | ); 36 | } 37 | 38 | $encodedId = urlencode((string) Util\Util::utf8($id)); 39 | $base = static::classUrl(); 40 | 41 | return "{$base}/{$encodedId}"; 42 | } 43 | 44 | public function refresh(): static 45 | { 46 | $requestor = new PrintNodeApiRequestor($this->_opts->apiKey, static::baseUrl()); 47 | $url = $this->instanceUrl(); 48 | 49 | /** @var \Rawilk\Printing\Api\PrintNode\PrintNodeApiResponse $response */ 50 | [$this->_opts->apiKey, $response] = $requestor->request( 51 | 'get', 52 | $url, 53 | headers: $this->_opts->headers, 54 | ); 55 | 56 | $this->setLastResponse($response); 57 | 58 | // Most responses from PrintNode come as a collection, so we usually need 59 | // the first item. 60 | $data = Util\Util::isList($response->body) 61 | ? $response->body[0] 62 | : $response->body; 63 | 64 | $this->refreshFrom($data, $this->_opts); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @return string the full API path for this API resource 71 | */ 72 | public function instanceUrl(): string 73 | { 74 | return static::resourceUrl($this['id']); 75 | } 76 | 77 | protected static function buildPath(string $basePath, int ...$ids): string 78 | { 79 | $ids = implode(',', array_map('urlencode', $ids)); 80 | 81 | return sprintf($basePath, $ids); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/TypeTag.php: -------------------------------------------------------------------------------- 1 | 41 | */ 42 | public function getClass(): string 43 | { 44 | return match ($this) { 45 | self::Charset => Types\Charset::class, 46 | self::NaturalLanguage => Types\NaturalLanguage::class, 47 | self::OctetString => Types\Primitive\OctetString::class, 48 | self::Integer => Types\Primitive\Integer::class, 49 | self::DateTime => Types\DateTime::class, 50 | self::NoValue => Types\Primitive\NoValue::class, 51 | self::NameWithoutLanguage => Types\NameWithoutLanguage::class, 52 | self::Uri => Types\Uri::class, 53 | self::Boolean => Types\Primitive\Boolean::class, 54 | self::Enum => Types\Primitive\Enum::class, 55 | self::TextWithoutLanguage => Types\TextWithoutLanguage::class, 56 | self::Keyword => Types\Primitive\Keyword::class, 57 | self::Unknown => Types\Primitive\Unknown::class, 58 | self::MimeMediaType => Types\MimeMedia::class, 59 | self::Resolution => Types\Resolution::class, 60 | self::RangeOfInteger => Types\RangeOfInteger::class, 61 | self::Collection => Types\Collection::class, 62 | self::Member => Types\Member::class, 63 | self::Text => Types\Primitive\Text::class, 64 | default => throw new UnknownType('Unknown type') 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Drivers/Cups/Entity/Printer.php: -------------------------------------------------------------------------------- 1 | printer->__debugInfo(); 28 | } 29 | 30 | public function printer(): CupsPrinter 31 | { 32 | return $this->printer; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function capabilities(): array 39 | { 40 | return $this->printer->capabilities(); 41 | } 42 | 43 | public function description(): ?string 44 | { 45 | return $this->printer->printerInfo; 46 | } 47 | 48 | public function id(): string 49 | { 50 | return $this->printer->uri; 51 | } 52 | 53 | public function isOnline(): bool 54 | { 55 | return $this->printer->isOnline(); 56 | } 57 | 58 | public function name(): ?string 59 | { 60 | return $this->printer->printerName; 61 | } 62 | 63 | public function status(): string 64 | { 65 | return $this->printer->state()?->name; 66 | } 67 | 68 | public function trays(): array 69 | { 70 | return $this->printer->trays(); 71 | } 72 | 73 | public function jobs( 74 | array $params = [], 75 | array|null|RequestOptions $opts = null, 76 | ): Collection { 77 | return Printing::driver(PrintDriver::Cups) 78 | ->printerPrintJobs($this->id(), null, null, null, $params, $opts); 79 | } 80 | 81 | public function toArray(): array 82 | { 83 | return [ 84 | 'id' => $this->id(), 85 | 'name' => $this->name(), 86 | 'description' => $this->description(), 87 | 'online' => $this->isOnline(), 88 | 'status' => $this->status(), 89 | 'trays' => $this->trays(), 90 | 'capabilities' => $this->capabilities(), 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Drivers/PrintNode/Entity/Printer.php: -------------------------------------------------------------------------------- 1 | printer->__debugInfo(); 27 | } 28 | 29 | public function printer(): PrintNodePrinter 30 | { 31 | return $this->printer; 32 | } 33 | 34 | public function capabilities(): array 35 | { 36 | return $this->printer->capabilities->toArray(); 37 | } 38 | 39 | public function printerCapabilities(): PrinterCapabilities 40 | { 41 | return $this->printer->capabilities; 42 | } 43 | 44 | public function description(): ?string 45 | { 46 | return $this->printer->description; 47 | } 48 | 49 | public function id(): int 50 | { 51 | return $this->printer->id; 52 | } 53 | 54 | public function isOnline(): bool 55 | { 56 | return $this->printer->isOnline(); 57 | } 58 | 59 | public function name(): ?string 60 | { 61 | return $this->printer->name; 62 | } 63 | 64 | public function status(): string 65 | { 66 | return $this->printer->state; 67 | } 68 | 69 | public function trays(): array 70 | { 71 | return $this->printer->trays(); 72 | } 73 | 74 | /** 75 | * @return Collection 76 | */ 77 | public function jobs(?array $params = null, null|array|RequestOptions $opts = null): Collection 78 | { 79 | return $this->printer->printJobs($params, $opts)->mapInto(PrintJob::class); 80 | } 81 | 82 | public function toArray(): array 83 | { 84 | return [ 85 | 'id' => $this->id(), 86 | 'name' => $this->name(), 87 | 'description' => $this->description(), 88 | 'online' => $this->isOnline(), 89 | 'status' => $this->status(), 90 | 'trays' => $this->trays(), 91 | 'capabilities' => $this->capabilities(), 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/ApiOperations/Request.php: -------------------------------------------------------------------------------- 1 | body, $opts, $expectedResource); 34 | 35 | return collect($resources) 36 | ->flatten() 37 | ->transform(function (PrintNodeApiResource $resource) use ($response) { 38 | $resource->setLastResponse($response); 39 | 40 | return $resource; 41 | }); 42 | } 43 | 44 | protected static function _staticRequest( 45 | string $method, 46 | string $url, 47 | ?array $params = null, 48 | null|array|RequestOptions $opts = null, 49 | ): array { 50 | $opts = RequestOptions::parse($opts); 51 | $baseUrl = $opts->apiBase ?? static::baseUrl(); 52 | 53 | $requestor = new PrintNodeApiRequestor($opts->apiKey, $baseUrl); 54 | [$opts->apiKey, $response] = $requestor->request($method, $url, $params, $opts->headers); 55 | 56 | return [$response, $opts]; 57 | } 58 | 59 | protected function _request( 60 | string $method, 61 | string $url, 62 | ?array $params = null, 63 | null|array|RequestOptions $opts = null, 64 | ): array { 65 | $opts = $this->_opts->merge($opts); 66 | 67 | /** @var \Rawilk\Printing\Api\PrintNode\PrintNodeApiResponse $response */ 68 | [$response, $opts] = static::_staticRequest($method, $url, $params ?? [], $opts); 69 | 70 | $this->setLastResponse($response); 71 | 72 | return [$response->body, $opts]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Api/Cups/Service/PrinterService.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function all(array $params = [], array|null|RequestOptions $opts = null): Collection 25 | { 26 | $pendingRequest = (new PendingRequest) 27 | ->setVersion(Version::V2_1) 28 | ->setOperation(Operation::CupsGetPrinters); 29 | 30 | return $this->request($pendingRequest, $opts)->printers(); 31 | } 32 | 33 | /** 34 | * $params is unused for now, but may be utilized later. 35 | */ 36 | public function retrieve(string $uri, array $params = [], array|null|RequestOptions $opts = null): ?Printer 37 | { 38 | $pendingRequest = (new PendingRequest) 39 | ->setVersion(Version::V2_1) 40 | ->setOperation(Operation::GetPrinterAttributes) 41 | ->addOperationAttributes([ 42 | OperationAttribute::PrinterUri->value => OperationAttribute::PrinterUri->toType($uri), 43 | ]); 44 | 45 | $response = $this->request($pendingRequest, $opts); 46 | 47 | return $response->printers()->first(); 48 | } 49 | 50 | public function printJobs(string $parentUri, array $params = [], array|null|RequestOptions $opts = null): Collection 51 | { 52 | $whichJobs = data_get($params, 'state', 'not-completed'); 53 | unset($params['state']); 54 | 55 | $pendingRequest = (new PendingRequest) 56 | ->setVersion(Version::V2_1) 57 | ->setOperation(Operation::GetJobs) 58 | ->addOperationAttributes([ 59 | OperationAttribute::PrinterUri->value => OperationAttribute::PrinterUri->toType($parentUri), 60 | OperationAttribute::WhichJobs->value => OperationAttribute::WhichJobs->toType($whichJobs), 61 | OperationAttribute::RequestedAttributes->value => $params[OperationAttribute::RequestedAttributes->value] ?? PrintJob::defaultRequestedAttributes(), 62 | 63 | ...Arr::except($params, OperationAttribute::RequestedAttributes->value), 64 | ]); 65 | 66 | return $this->request($pendingRequest, $opts)->jobs(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/Computer.php: -------------------------------------------------------------------------------- 1 | parseDate($this->createTimestamp); 39 | } 40 | 41 | /** 42 | * Fetch all printers attached to the computer. 43 | * 44 | * @return Collection 45 | */ 46 | public function printers(?array $params = null, null|array|RequestOptions $opts = null): Collection 47 | { 48 | $url = $this->instanceUrl() . '/printers'; 49 | 50 | return static::_requestPage($url, $params ?? [], $opts, expectedResource: Printer::class); 51 | } 52 | 53 | /** 54 | * Find a specific printer attached to the computer. Pass an array for `$id` to find a set of 55 | * printers. 56 | * 57 | * @return null|Printer|Collection 58 | */ 59 | public function findPrinter( 60 | int|array $id, 61 | ?array $params = null, 62 | null|array|RequestOptions $opts = null 63 | ): null|Printer|Collection { 64 | $path = is_array($id) 65 | ? static::buildPath('/printers/%s', ...$id) 66 | : static::buildPath('/printers/%s', $id); 67 | 68 | $url = $this->instanceUrl() . $path; 69 | 70 | $printers = static::_requestPage($url, $params ?? [], $opts, expectedResource: Printer::class); 71 | 72 | return is_array($id) ? $printers : $printers->first(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/Support/PrinterCapabilities.php: -------------------------------------------------------------------------------- 1 | N-up printing is supported, or a 28 | * zero-length array if N-up printing is not supported. 29 | * @property-read array $papers The paper sizes that are supported by the printer. Each key represents a paper name 30 | * and the corresponding value is the dimension of the paper expressed in a two-value array. The array is 31 | * expressed as `[width, height]`, with `width` and `height` expressed in tenths of a mm. In some 32 | * circumstances these values are not reported by the printer driver, in which case the array 33 | * is `[null, null]`. 34 | * @property-read null|\Rawilk\Printing\Api\PrintNode\Resources\Support\PrintRate $printrate The printer's supported print rate. 35 | * @property-read bool $supports_custom_paper_size Indicates `true` if the printer supports custom paper sizes. 36 | */ 37 | class PrinterCapabilities extends PrintNodeObject 38 | { 39 | // Alias for bins 40 | public function trays(): array 41 | { 42 | return $this->bins; 43 | } 44 | 45 | protected function getExpectedValueResource(string $key): ?string 46 | { 47 | return match ($key) { 48 | 'printrate' => PrintRate::class, 49 | default => null, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/Operation.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function capabilities(): array 29 | { 30 | return array_filter( 31 | $this->_values, 32 | fn (string $key): bool => ! in_array($key, [ 33 | 'printer-uri-supported', 34 | 'uri', 35 | 'printer-state', 36 | 'printer-name', 37 | 'printer-info', 38 | ], true), 39 | ARRAY_FILTER_USE_KEY, 40 | ); 41 | } 42 | 43 | public function state(): ?PrinterState 44 | { 45 | return PrinterState::tryFrom($this->printerState); 46 | } 47 | 48 | /** 49 | * @return Collection 50 | */ 51 | public function stateReasons(): Collection 52 | { 53 | return collect($this->printerStateReasons) 54 | ->map(fn (string $reason) => PrinterStateReason::tryFrom($reason)) 55 | ->filter(); 56 | } 57 | 58 | public function isOnline(): bool 59 | { 60 | // First check if any of the reported state reasons are "offline". 61 | $offline = $this->stateReasons()->first( 62 | fn (PrinterStateReason $reason): bool => $reason->isOffline() 63 | ); 64 | 65 | if ($offline) { 66 | return false; 67 | } 68 | 69 | return $this->state()?->isOnline() ?? false; 70 | } 71 | 72 | public function trays(): array 73 | { 74 | return $this->mediaSourceSupported ?? []; 75 | } 76 | 77 | protected function mutateAttributes(array $values): array 78 | { 79 | $values['printer-uri-supported'] = $this->attributeValue($values, 'printer-uri-supported'); 80 | $values['printer-state'] = $this->attributeValue($values, 'printer-state', PrinterState::Stopped->value); 81 | $values['printer-name'] = $this->attributeValue($values, 'printer-name'); 82 | $values['media-source-supported'] = $this->attributeValue($values, 'media-source-supported', []); 83 | $values['printer-info'] = $this->attributeValue($values, 'printer-info'); 84 | $values['printer-state-reasons'] = data_get($values, 'printer-state-reasons', []); 85 | 86 | return $values; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /config/printing.php: -------------------------------------------------------------------------------- 1 | env('PRINTING_DRIVER', PrintDriver::PrintNode->value), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Drivers 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Configuration for each driver. 24 | | 25 | */ 26 | 'drivers' => [ 27 | PrintDriver::PrintNode->value => [ 28 | 'key' => env('PRINT_NODE_API_KEY'), 29 | ], 30 | 31 | PrintDriver::Cups->value => [ 32 | 'ip' => env('CUPS_SERVER_IP'), 33 | 'username' => env('CUPS_SERVER_USERNAME'), 34 | 'password' => env('CUPS_SERVER_PASSWORD'), 35 | 'port' => (int) env('CUPS_SERVER_PORT'), 36 | 'secure' => env('CUPS_SERVER_SECURE'), 37 | ], 38 | 39 | /* 40 | * Add your custom drivers here: 41 | * 42 | * 'custom' => [ 43 | * 'driver' => 'custom_driver', 44 | * // other config for your custom driver 45 | * ], 46 | */ 47 | ], 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Default Printer Id 52 | |-------------------------------------------------------------------------- 53 | | 54 | | If you know the id of a default printer you want to use, enter it here. 55 | | 56 | */ 57 | 'default_printer_id' => null, 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Receipt Printer Options 62 | |-------------------------------------------------------------------------- 63 | | 64 | */ 65 | 'receipts' => [ 66 | /* 67 | * How many characters fit across a single line on the receipt paper. 68 | * Adjust according to your needs. 69 | */ 70 | 'line_character_length' => 45, 71 | 72 | /* 73 | * The width of the print area in dots. 74 | * Adjust according to your needs. 75 | */ 76 | 'print_width' => 550, 77 | 78 | /* 79 | * The height (in dots) barcodes should be printed normally. 80 | */ 81 | 'barcode_height' => 64, 82 | 83 | /* 84 | * The width (magnification) each barcode should be printed in normally. 85 | */ 86 | 'barcode_width' => 2, 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Printing Logger 92 | |-------------------------------------------------------------------------- 93 | | 94 | | This setting defines which logging channel will be used by this package 95 | | to write log messages. You are free to specify any of your logging 96 | | channels listed inside the "logging" configuration file. 97 | | 98 | */ 99 | 'logger' => env('PRINTING_LOGGER'), 100 | ]; 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Printing for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/rawilk/laravel-printing.svg?style=flat-square)](https://packagist.org/packages/rawilk/laravel-printing) 4 | ![Tests](https://github.com/rawilk/laravel-printing/workflows/Tests/badge.svg?style=flat-square) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/rawilk/laravel-printing.svg?style=flat-square)](https://packagist.org/packages/rawilk/laravel-printing) 6 | [![PHP from Packagist](https://img.shields.io/packagist/php-v/rawilk/laravel-printing?style=flat-square)](https://packagist.org/packages/rawilk/laravel-printing) 7 | [![License](https://img.shields.io/github/license/rawilk/laravel-printing?style=flat-square)](https://github.com/rawilk/laravel-printing/blob/main/LICENSE.md) 8 | 9 | ![social image](https://banners.beyondco.de/Printing%20for%20Laravel.png?theme=light&packageManager=composer+require&packageName=rawilk%2Flaravel-printing&pattern=parkayFloor&style=style_1&description=Direct+printing+for+Laravel+apps.&md=1&showWatermark=0&fontSize=100px&images=printer) 10 | 11 | Printing for Laravel allows your application to directly send PDF documents or raw text directly from a remote server 12 | to a printer on your local network. Receipts can also be printed by first generating the raw text via the `Rawilk\Printing\Receipts\ReceiptPrinter` class, and then sending the text as a raw print job via the `Printing` facade. 13 | 14 | ```php 15 | $printJob = Printing::newPrintTask() 16 | ->printer($printerId) 17 | ->file('path_to_file.pdf') 18 | ->send(); 19 | 20 | $printJob->id(); // the id number returned from the print server 21 | ``` 22 | 23 | Supported Print Drivers: 24 | 25 | - PrintNode: https://printnode.com 26 | - CUPS: https://cups.org 27 | - Custom: Configure your own custom driver 28 | 29 | ## Documentation: 30 | 31 | For documentation, please visit: https://randallwilk.dev/docs/laravel-printing 32 | 33 | ## Installation 34 | 35 | You can install the package via composer: 36 | 37 | ```bash 38 | composer require rawilk/laravel-printing 39 | ``` 40 | 41 | You can publish the config file with: 42 | 43 | ```bash 44 | php artisan vendor:publish --tag="printing-config" 45 | ``` 46 | 47 | The contents of the default configuration file can be found here: https://github.com/rawilk/laravel-printing/blob/main/config/printing.php 48 | 49 | ## Testing 50 | 51 | ```bash 52 | composer test 53 | ``` 54 | 55 | ## Changelog 56 | 57 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 58 | 59 | ## Contributing 60 | 61 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 62 | 63 | ## Security 64 | 65 | If you discover any security related issues, please email randall@randallwilk.dev instead of using the issue tracker. 66 | 67 | ## Credits 68 | 69 | - [Randall Wilk](https://github.com/rawilk) 70 | - [All Contributors](../../contributors) 71 | - _Mike42_ for the [PHP ESC/POS Print Driver](https://github.com/mike42/escpos-php) library 72 | 73 | Inspiration for the PrintNode API wrapper comes from: 74 | 75 | - [PrintNode/PrintNode-PHP](https://github.com/PrintNode/PrintNode-PHP) 76 | - [phatkoala/printnode](https://github.com/PhatKoala/PrintNode) 77 | 78 | Inspiration for certain aspects of the API implementations comes from: 79 | 80 | - [stripe-php](https://github.com/stripe/stripe-php) 81 | 82 | ## Disclaimer 83 | 84 | This package is not affiliated with, maintained, authorized, endorsed or sponsored by Laravel or any of its affiliates. 85 | 86 | ## License 87 | 88 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 89 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/PrintJob.php: -------------------------------------------------------------------------------- 1 | toArray() : $params; 39 | 40 | $url = static::classUrl(); 41 | 42 | /** @var \Rawilk\Printing\Api\PrintNode\PrintNodeApiResponse $response */ 43 | [$response, $opts] = static::_staticRequest('post', $url, $data, $opts); 44 | 45 | // PrintNode only returns the ID of the new job, so we need to perform another api call 46 | // to fetch the new job, unfortunately. 47 | $jobId = $response->body; 48 | 49 | throw_unless( 50 | filled($jobId) && is_int($jobId), 51 | PrintTaskFailed::noJobCreated(), 52 | ); 53 | 54 | $instance = new static($jobId, $opts); 55 | $instance->refresh(); 56 | 57 | $instance->setLastResponse($response); 58 | 59 | return $instance; 60 | } 61 | 62 | public function createdAt(): ?CarbonInterface 63 | { 64 | return $this->parseDate($this->createTimestamp); 65 | } 66 | 67 | public function expiresAt(): ?CarbonInterface 68 | { 69 | return $this->parseDate($this->expireAt); 70 | } 71 | 72 | /** 73 | * Alias for `delete()`. 74 | */ 75 | public function cancel(?array $params = null, null|array|RequestOptions $opts = null): static 76 | { 77 | return $this->delete($params, $opts); 78 | } 79 | 80 | /** 81 | * Get all the states that PrintNode has reported for the job. 82 | * 83 | * @return Collection 84 | */ 85 | public function getStates(?array $params = null, null|array|RequestOptions $opts = null): Collection 86 | { 87 | $url = $this->instanceUrl() . '/states'; 88 | 89 | return static::_requestPage($url, $params ?? [], $opts, expectedResource: PrintJobState::class); 90 | } 91 | 92 | protected function getExpectedValueResource(string $key): ?string 93 | { 94 | return match ($key) { 95 | 'printer' => Printer::class, 96 | default => null, 97 | }; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/PrintTask.php: -------------------------------------------------------------------------------- 1 | printSource = config('app.name'); 34 | } 35 | 36 | public function content($content): static 37 | { 38 | $this->content = $content; 39 | 40 | return $this; 41 | } 42 | 43 | public function file(string $filePath): static 44 | { 45 | throw_unless( 46 | file_exists($filePath), 47 | InvalidSource::fileNotFound($filePath), 48 | ); 49 | 50 | try { 51 | $content = file_get_contents($filePath); 52 | } catch (Throwable) { 53 | throw InvalidSource::cannotOpenFile($filePath); 54 | } 55 | 56 | if (blank($content)) { 57 | Printing::getLogger()?->error("No content retrieved from file: {$filePath}"); 58 | } 59 | 60 | $this->content = $content; 61 | 62 | return $this; 63 | } 64 | 65 | public function url(string $url): static 66 | { 67 | throw_unless( 68 | preg_match('/^https?:\/\//', $url), 69 | InvalidSource::invalidUrl($url), 70 | ); 71 | 72 | $this->content = file_get_contents($url); 73 | 74 | return $this; 75 | } 76 | 77 | public function jobTitle(string $jobTitle): static 78 | { 79 | $this->jobTitle = $jobTitle; 80 | 81 | return $this; 82 | } 83 | 84 | public function printer(Printer|string|null|int $printerId): static 85 | { 86 | if ($printerId instanceof Printer) { 87 | $printerId = $printerId->id(); 88 | } 89 | 90 | $this->printerId = $printerId; 91 | 92 | return $this; 93 | } 94 | 95 | public function printSource(string $printSource): static 96 | { 97 | $this->printSource = $printSource; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Not all drivers may support tagging jobs. 104 | */ 105 | public function tags($tags): static 106 | { 107 | return $this; 108 | } 109 | 110 | /** 111 | * Not all drivers may support this feature. 112 | */ 113 | public function tray($tray): static 114 | { 115 | return $this; 116 | } 117 | 118 | /** 119 | * Not all drivers might support this option. 120 | */ 121 | public function copies(int $copies): static 122 | { 123 | return $this; 124 | } 125 | 126 | public function option(string|BackedEnum $key, $value): static 127 | { 128 | $keyValue = $key instanceof BackedEnum ? $key->value : $key; 129 | 130 | $this->options[$keyValue] = $value; 131 | 132 | return $this; 133 | } 134 | 135 | protected function resolveJobTitle(): string 136 | { 137 | if ($this->jobTitle) { 138 | return $this->jobTitle; 139 | } 140 | 141 | return 'job_' . Str::random(8) . '_' . date('Ymdhis'); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Util/Util.php: -------------------------------------------------------------------------------- 1 | $expectedResource the expected resource class for the response 38 | */ 39 | public static function convertToPrintNodeObject(mixed $response, array|null|RequestOptions $opts, ?string $expectedResource = null): mixed 40 | { 41 | if (self::isList($response)) { 42 | $mapped = []; 43 | 44 | foreach ($response as $responseValue) { 45 | $mapped[] = self::convertToPrintNodeObject($responseValue, $opts, $expectedResource); 46 | } 47 | 48 | return $mapped; 49 | } 50 | 51 | if (is_array($response) && $expectedResource !== null) { 52 | throw_unless( 53 | class_exists($expectedResource), 54 | InvalidArgument::class, 55 | 'PrintNode resource class "' . $expectedResource . '" does not exist', 56 | ); 57 | 58 | return $expectedResource::make($response, $opts); 59 | } 60 | 61 | return $response; 62 | } 63 | 64 | public static function normalizeId(mixed $id): array 65 | { 66 | if (is_array($id)) { 67 | if (! isset($id['id'])) { 68 | return [null, $id]; 69 | } 70 | 71 | $params = $id; 72 | $id = $params['id']; 73 | unset($params['id']); 74 | } else { 75 | $params = []; 76 | } 77 | 78 | return [$id, $params]; 79 | } 80 | 81 | /** 82 | * @param mixed|string $value a string to UTF-8 encode 83 | * @return mixed|string the UTF-8 encoded string, or the object passed in if it wasn't a string 84 | */ 85 | public static function utf8(mixed $value): mixed 86 | { 87 | if (self::$isMbstringAvailable === null) { 88 | self::$isMbstringAvailable = function_exists('mb_detect_encoding') 89 | && function_exists('mb_convert_encoding'); 90 | 91 | if (! self::$isMbstringAvailable) { 92 | trigger_error( 93 | <<<'TXT' 94 | It looks like the mbstring extension is not enabled. 95 | UTF-8 strings will not be properly encoded. Ask your 96 | system administrator to enable the mbstring extension. 97 | TXT, 98 | E_USER_WARNING, 99 | ); 100 | } 101 | } 102 | 103 | if ( 104 | is_string($value) && 105 | self::$isMbstringAvailable && 106 | mb_detect_encoding($value, 'UTF-8', true) !== 'UTF-8' 107 | ) { 108 | return mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); 109 | } 110 | 111 | return $value; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Api/Cups/PendingRequest.php: -------------------------------------------------------------------------------- 1 | addOperationAttributes([ 33 | 'attributes-charset' => new Charset('utf-8'), 34 | 'attributes-natural-language' => new NaturalLanguage('en'), 35 | ]); 36 | } 37 | 38 | public function setVersion(Version $version): static 39 | { 40 | $this->version = $version; 41 | 42 | return $this; 43 | } 44 | 45 | public function setOperation(int|Operation $operation): static 46 | { 47 | $this->operation = $operation instanceof Operation ? $operation->value : $operation; 48 | 49 | return $this; 50 | } 51 | 52 | public function setContent(string $content): static 53 | { 54 | $this->content = $content; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * You may optionally specify the request ID, default is 1 61 | */ 62 | public function setRequestId(int $requestId): static 63 | { 64 | $this->requestId = $requestId; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @param array $attributes 71 | */ 72 | public function addOperationAttributes(array $attributes): static 73 | { 74 | $this->setAttributes(OperationGroup::class, $attributes); 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * @param array $attributes 81 | */ 82 | public function addJobAttributes(array $attributes): static 83 | { 84 | $this->setAttributes(JobGroup::class, $attributes); 85 | 86 | return $this; 87 | } 88 | 89 | public function encode(): string 90 | { 91 | $binary = $this->version->encode(); 92 | $binary .= pack('n', $this->operation); 93 | $binary .= pack('N', $this->requestId); 94 | 95 | foreach ($this->attributeGroups as $group) { 96 | $binary .= $group->encode(); 97 | } 98 | 99 | $binary .= pack('c', AttributeGroupTag::EndOfAttributes->value); 100 | 101 | if ($this->content) { 102 | $binary .= $this->content; 103 | } 104 | 105 | return $binary; 106 | } 107 | 108 | protected function setAttributes(string $className, array $attributes): void 109 | { 110 | $index = $this->getGroupIndex($className); 111 | 112 | foreach ($attributes as $name => $value) { 113 | $this->attributeGroups[$index]->{$name} = $value; 114 | } 115 | } 116 | 117 | protected function getGroupIndex(string $className): int 118 | { 119 | foreach ($this->attributeGroups as $index => $attributeGroup) { 120 | if ($attributeGroup instanceof $className) { 121 | return $index; 122 | } 123 | } 124 | 125 | $this->attributeGroups[] = new $className; 126 | 127 | return count($this->attributeGroups) - 1; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Util/RequestOptions.php: -------------------------------------------------------------------------------- 1 | $this->redactedApiKey(), 23 | 'headers' => $this->headers, 24 | 'apiBase' => $this->apiBase, 25 | ]; 26 | } 27 | 28 | /** 29 | * Unpacks an options array into a RequestOptions object. 30 | * 31 | * @param bool $strict when true, forbid string form and arbitrary keys in array form 32 | */ 33 | public static function parse(RequestOptions|array|string|null $options, bool $strict = false): self 34 | { 35 | if ($options instanceof self) { 36 | return clone $options; 37 | } 38 | 39 | if ($options === null) { 40 | return new self(null, [], null); 41 | } 42 | 43 | if (is_string($options)) { 44 | throw_if( 45 | $strict, 46 | InvalidArgument::class, 47 | <<<'TXT' 48 | Do not pass a string for request options. If you want to set 49 | the API key, pass an array like ["api_key" => ] instead. 50 | TXT 51 | ); 52 | 53 | return new self($options, [], null); 54 | } 55 | 56 | if (is_array($options)) { 57 | $headers = []; 58 | $key = null; 59 | $base = null; 60 | 61 | if (array_key_exists('api_key', $options)) { 62 | $key = $options['api_key']; 63 | unset($options['api_key']); 64 | } 65 | 66 | if (array_key_exists('idempotency_key', $options)) { 67 | $headers['X-Idempotency-Key'] = $options['idempotency_key']; 68 | unset($options['idempotency_key']); 69 | } 70 | 71 | if (array_key_exists('api_base', $options)) { 72 | $base = $options['api_base']; 73 | unset($options['api_base']); 74 | } 75 | 76 | if ($strict && ! empty($options)) { 77 | $message = 'Got unexpected keys in options array: ' . implode(', ', array_keys($options)); 78 | 79 | throw new InvalidArgument($message); 80 | } 81 | 82 | return new self($key, $headers, $base); 83 | } 84 | 85 | throw new InvalidArgument('Unexpected value received for request options.'); 86 | } 87 | 88 | /** 89 | * Unpacks an options array and merges it into the existing RequestOptions object. 90 | * 91 | * @param bool $strict when true, forbid string form and arbitrary keys in array form 92 | */ 93 | public function merge(RequestOptions|array|null|string $options, bool $strict = false): self 94 | { 95 | $otherOptions = self::parse($options, $strict); 96 | if ($otherOptions->apiKey === null) { 97 | $otherOptions->apiKey = $this->apiKey; 98 | } 99 | 100 | if ($otherOptions->apiBase === null) { 101 | $otherOptions->apiBase = $this->apiBase; 102 | } 103 | 104 | $otherOptions->headers = array_merge($this->headers, $otherOptions->headers); 105 | 106 | return $otherOptions; 107 | } 108 | 109 | private function redactedApiKey(): string 110 | { 111 | if ($this->apiKey === null) { 112 | return ''; 113 | } 114 | 115 | return Str::mask($this->apiKey, '*', 4); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | An array callback functions to create custom drivers. 22 | */ 23 | protected array $customCreators = []; 24 | 25 | public function __construct(#[SensitiveParameter] protected array $config) 26 | { 27 | } 28 | 29 | public function driver(null|string|PrintDriver $driver = null): Driver 30 | { 31 | if ($driver instanceof BackedEnum) { 32 | $driver = (string) $driver->value; 33 | } 34 | 35 | if (blank($driver)) { 36 | $driver = $this->getDefaultDriverName(); 37 | } 38 | 39 | return $this->drivers[$driver] = $this->get($driver); 40 | } 41 | 42 | public function extend(string $driver, Closure $callback): self 43 | { 44 | $this->customCreators[$driver] = $callback; 45 | 46 | return $this; 47 | } 48 | 49 | public function getConfig(): array 50 | { 51 | return $this->config; 52 | } 53 | 54 | public function updateConfig(array $config): void 55 | { 56 | $this->config = array_replace_recursive($this->config, $config); 57 | 58 | // Reset our drivers for potential changes to credentials. 59 | $this->drivers = []; 60 | } 61 | 62 | protected function createCupsDriver(#[SensitiveParameter] array $config): Driver 63 | { 64 | PrintDriver::Cups->ensureConfigIsValid($config); 65 | 66 | return new Drivers\Cups\Cups($config); 67 | } 68 | 69 | protected function createPrintnodeDriver(#[SensitiveParameter] array $config): Driver 70 | { 71 | PrintDriver::PrintNode->ensureConfigIsValid($config); 72 | 73 | return new Drivers\PrintNode\PrintNode($config['key'] ?? null); 74 | } 75 | 76 | protected function get(string $driver): Driver 77 | { 78 | return $this->drivers[$driver] ?? $this->resolve($driver); 79 | } 80 | 81 | protected function getDefaultDriverName(): string 82 | { 83 | return $this->config['driver'] ?? PrintDriver::PrintNode->value; 84 | } 85 | 86 | protected function getDriverConfig(string $driver): ?array 87 | { 88 | return Arr::get($this->config, "drivers.{$driver}"); 89 | } 90 | 91 | protected function resolve(string $driver): Driver 92 | { 93 | if (Arr::has($this->drivers, $driver)) { 94 | return $this->drivers[$driver]; 95 | } 96 | 97 | $config = $this->getDriverConfig($driver); 98 | 99 | if ($this->hasCustomCreator($config['driver'] ?? $driver)) { 100 | return $this->callCustomCreator($config, $config['driver'] ?? $driver); 101 | } 102 | 103 | $method = 'create' . ucfirst($driver) . 'Driver'; 104 | 105 | throw_unless( 106 | method_exists($this, $method), 107 | UnsupportedDriver::driver($driver), 108 | ); 109 | 110 | throw_unless( 111 | is_array($config), 112 | DriverConfigNotFound::forDriver($driver), 113 | ); 114 | 115 | return $this->$method($config); 116 | } 117 | 118 | protected function hasCustomCreator(string $driver): bool 119 | { 120 | return Arr::has($this->customCreators, $driver); 121 | } 122 | 123 | protected function callCustomCreator(?array $config, string $driver): Driver 124 | { 125 | return $this->customCreators[$driver]($config); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Api/Cups/AttributeGroup.php: -------------------------------------------------------------------------------- 1 | attributes[$name] = $value; 29 | } 30 | 31 | /** 32 | * @return array> 33 | */ 34 | public function getAttributes(): array 35 | { 36 | return $this->attributes; 37 | } 38 | 39 | public function encode(): string 40 | { 41 | $binary = pack('c', $this->tag); 42 | 43 | foreach ($this->attributes as $name => $value) { 44 | if (is_array($value)) { 45 | $binary .= $this->handleArrayEncode($name, $value); 46 | 47 | continue; 48 | } 49 | 50 | throw_unless( 51 | $value instanceof Type, 52 | TypeNotSpecified::class, 53 | 'Attribute value has to be of type ' . Type::class, 54 | ); 55 | 56 | $nameLen = strlen($name); 57 | $binary .= pack('c', $value->getTag()); 58 | 59 | $binary .= pack('n', $nameLen); // Attribute key length 60 | $binary .= pack('a' . $nameLen, $name); // Attribute key 61 | 62 | $binary .= $value->encode(); // Attribute value (with length) 63 | } 64 | 65 | return $binary; 66 | } 67 | 68 | // region ArrayAccess 69 | public function offsetExists(mixed $offset): bool 70 | { 71 | return array_key_exists($offset, $this->attributes); 72 | } 73 | 74 | public function offsetGet(mixed $offset): mixed 75 | { 76 | return $this->attributes[$offset] ?? null; 77 | } 78 | 79 | public function offsetSet(mixed $offset, mixed $value): void 80 | { 81 | $this->attributes[$offset] = $value; 82 | } 83 | 84 | public function offsetUnset(mixed $offset): void 85 | { 86 | unset($this->attributes[$offset]); 87 | } 88 | // endregion 89 | 90 | public function toArray(): array 91 | { 92 | return $this->attributes; 93 | } 94 | 95 | public function jsonSerialize(): mixed 96 | { 97 | return $this->toArray(); 98 | } 99 | 100 | /** 101 | * If attribute is an array, the attribute name after the first element is empty 102 | * 103 | * @param array $values 104 | */ 105 | private function handleArrayEncode(string $name, array $values): string 106 | { 107 | $str = ''; 108 | 109 | if ($values[0] instanceof RangeOfInteger) { 110 | RangeOfInteger::checkOverlaps($values); 111 | } 112 | 113 | foreach ($values as $i => $iValue) { 114 | $_name = $name; 115 | 116 | if ($i !== 0) { 117 | $_name = ''; 118 | } 119 | 120 | $nameLen = strlen($_name); 121 | 122 | $str .= pack('c', $iValue->getTag()); // Value tag 123 | $str .= pack('n', $nameLen); // Attribute key length 124 | $str .= pack('a' . $nameLen, $_name); // Attribute key 125 | 126 | $str .= $iValue->encode(); 127 | } 128 | 129 | return $str; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Resources/Printer.php: -------------------------------------------------------------------------------- 1 | parseDate($this->createTimestamp); 35 | } 36 | 37 | public function copies(): int 38 | { 39 | return $this->capabilities?->copies ?? 1; 40 | } 41 | 42 | public function isColor(): bool 43 | { 44 | return $this->capabilities?->color === true; 45 | } 46 | 47 | public function canCollate(): bool 48 | { 49 | return $this->capabilities?->collate ?? false; 50 | } 51 | 52 | public function media(): array 53 | { 54 | return $this->capabilities?->medias ?? []; 55 | } 56 | 57 | public function bins(): array 58 | { 59 | return $this->capabilities?->bins ?? []; 60 | } 61 | 62 | // Alias for bins() 63 | public function trays(): array 64 | { 65 | return $this->bins(); 66 | } 67 | 68 | public function isOnline(): bool 69 | { 70 | return strtolower($this->state) === 'online'; 71 | } 72 | 73 | /** 74 | * Fetch all print jobs that have been sent to the printer. 75 | * 76 | * @return Collection 77 | */ 78 | public function printJobs(?array $params = null, null|array|RequestOptions $opts = null): Collection 79 | { 80 | $url = $this->instanceUrl() . '/printjobs'; 81 | 82 | return static::_requestPage($url, $params ?? [], $opts, expectedResource: PrintJob::class); 83 | } 84 | 85 | /** 86 | * Find a specific job that was sent to the printer. Pass an array for `$id` to find a set 87 | * of jobs. 88 | * 89 | * @return null|PrintJob|Collection 90 | */ 91 | public function findPrintJob( 92 | int|array $id, 93 | ?array $params = null, 94 | null|array|RequestOptions $opts = null 95 | ): null|PrintJob|Collection { 96 | $path = is_array($id) 97 | ? static::buildPath('/printjobs/%s', ...$id) 98 | : static::buildPath('/printjobs/%s', $id); 99 | 100 | $url = $this->instanceUrl() . $path; 101 | 102 | $jobs = static::_requestPage($url, $params ?? [], $opts, expectedResource: PrintJob::class); 103 | 104 | return is_array($id) ? $jobs : $jobs->first(); 105 | } 106 | 107 | protected function getExpectedValueResource(string $key): ?string 108 | { 109 | return match ($key) { 110 | 'computer' => Computer::class, 111 | 'capabilities' => PrinterCapabilities::class, 112 | default => null, 113 | }; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Drivers/PrintNode/PrintNode.php: -------------------------------------------------------------------------------- 1 | client = app(PrintNodeClient::class, ['config' => ['api_key' => $apiKey]]); 25 | } 26 | 27 | public function getApiKey(): ?string 28 | { 29 | return $this->client->getApiKey(); 30 | } 31 | 32 | public function setApiKey(?string $apiKey): static 33 | { 34 | $this->client->setApiKey($apiKey); 35 | 36 | return $this; 37 | } 38 | 39 | public function newPrintTask(): PrintTask 40 | { 41 | return new PrintTask($this->client); 42 | } 43 | 44 | public function printer($printerId = null, ?array $params = null, null|array|RequestOptions $opts = null): ?PrinterContract 45 | { 46 | $printer = $this->client->printers->retrieve((int) $printerId, $params, $opts); 47 | 48 | if (! $printer) { 49 | return null; 50 | } 51 | 52 | return new PrinterContract($printer); 53 | } 54 | 55 | public function printers( 56 | ?int $limit = null, 57 | ?int $offset = null, 58 | ?string $dir = null, 59 | null|array|RequestOptions $opts = null, 60 | ): Collection { 61 | return $this->client->printers->all( 62 | params: static::formatPaginationParams($limit, $offset, $dir), 63 | opts: $opts, 64 | )->mapInto(PrinterContract::class); 65 | } 66 | 67 | public function printJobs( 68 | ?int $limit = null, 69 | ?int $offset = null, 70 | ?string $dir = null, 71 | null|array|RequestOptions $opts = null, 72 | ): Collection { 73 | return $this->client->printJobs->all( 74 | params: static::formatPaginationParams($limit, $offset, $dir), 75 | opts: $opts, 76 | )->mapInto(PrintJobContract::class); 77 | } 78 | 79 | public function printJob($jobId = null, ?array $params = null, null|array|RequestOptions $opts = null): ?PrintJobContract 80 | { 81 | $job = $this->client->printJobs->retrieve((int) $jobId, $params, $opts); 82 | 83 | if (! $job) { 84 | return null; 85 | } 86 | 87 | return new PrintJobContract($job); 88 | } 89 | 90 | public function printerPrintJobs( 91 | $printerId, 92 | ?int $limit = null, 93 | ?int $offset = null, 94 | ?string $dir = null, 95 | null|array|RequestOptions $opts = null, 96 | ): Collection { 97 | return $this->client->printers->printJobs( 98 | parentId: (int) $printerId, 99 | params: static::formatPaginationParams($limit, $offset, $dir), 100 | opts: $opts, 101 | )->mapInto(PrintJobContract::class); 102 | } 103 | 104 | public function printerPrintJob($printerId, $jobId, ?array $params = null, null|array|RequestOptions $opts = null): ?PrintJobContract 105 | { 106 | $job = $this->client->printers->printJob( 107 | parentId: (int) $printerId, 108 | printJobId: (int) $jobId, 109 | params: $params, 110 | opts: $opts, 111 | ); 112 | 113 | if (! $job) { 114 | return null; 115 | } 116 | 117 | return new PrintJobContract($job); 118 | } 119 | 120 | protected static function formatPaginationParams(?int $limit, ?int $offset, ?string $dir): array 121 | { 122 | return array_filter([ 123 | 'limit' => $limit, 124 | 'after' => $offset, 125 | 'dir' => $dir, 126 | ]); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Drivers/Cups/Cups.php: -------------------------------------------------------------------------------- 1 | client = app(CupsClient::class, ['config' => $config]); 26 | } 27 | 28 | public function getConfig(): array 29 | { 30 | return $this->client->getConfig(); 31 | } 32 | 33 | public function newPrintTask(): PrintTask 34 | { 35 | return new PrintTask($this->client); 36 | } 37 | 38 | public function printer($printerId = null, array $params = [], array|null|RequestOptions $opts = null): ?PrinterContract 39 | { 40 | $printer = $this->client->printers->retrieve($printerId, $params, $opts); 41 | 42 | if (! $printer) { 43 | return null; 44 | } 45 | 46 | return new PrinterContract($printer); 47 | } 48 | 49 | /** 50 | * CUPS doesn't support limit, offset 51 | * 52 | * Printers have a lot of attributes, without the requested attributes filter 53 | * the request will be about 2x slower 54 | * 55 | * @return \Illuminate\Support\Collection 56 | */ 57 | public function printers( 58 | ?int $limit = null, 59 | ?int $offset = null, 60 | ?string $dir = null, 61 | array $params = [], 62 | array|null|RequestOptions $opts = null, 63 | ): Collection { 64 | $printers = $this->client->printers->all($params, $opts); 65 | 66 | return $printers 67 | ->slice($offset ?? 0, $limit) 68 | ->values() 69 | ->mapInto(PrinterContract::class); 70 | } 71 | 72 | public function printJob($jobId = null, array $params = [], array|null|RequestOptions $opts = null): ?PrintJobContract 73 | { 74 | $job = $this->client->printJobs->retrieve($jobId, $params, $opts); 75 | 76 | if (! $job) { 77 | return null; 78 | } 79 | 80 | return new PrintJobContract($job); 81 | } 82 | 83 | /** 84 | * Note: $limit, $offset, $dir do nothing currently. 85 | */ 86 | public function printerPrintJobs( 87 | $printerId, 88 | ?int $limit = null, 89 | ?int $offset = null, 90 | ?string $dir = null, 91 | array $params = [], 92 | array|null|RequestOptions $opts = null, 93 | ): Collection { 94 | return $this->client->printers->printJobs( 95 | parentUri: $printerId, 96 | params: $params, 97 | opts: $opts, 98 | )->mapInto(PrintJobContract::class); 99 | } 100 | 101 | /** 102 | * There isn't really a way to do this with CUPS, but the normal `printJob()` method call 103 | * should yield the same result anyway. 104 | */ 105 | public function printerPrintJob($printerId, $jobId, array|null|RequestOptions $opts = null): ?PrintJobContract 106 | { 107 | return $this->printJob($jobId, $opts); 108 | } 109 | 110 | /** 111 | * Note: $limit, $offset occurs on the client side, $dir does nothing currently. 112 | * 113 | * @return \Illuminate\Support\Collection 114 | */ 115 | public function printJobs( 116 | ?int $limit = null, 117 | ?int $offset = null, 118 | ?string $dir = null, 119 | array $params = [], 120 | array|null|RequestOptions $opts = null, 121 | ): Collection { 122 | return $this->printers( 123 | params: $params, 124 | opts: $opts, 125 | ) 126 | ->map( 127 | fn (Printer $printer) => $this->printerPrintJobs($printer->id(), params: $params, opts: $opts) 128 | )->flatten(1)->skip($offset)->take($limit)->values(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Api/Cups/CupsRequestor.php: -------------------------------------------------------------------------------- 1 | encode(); 37 | } 38 | 39 | [$adminUrl, $username, $password] = $this->prepareRequest(); 40 | 41 | $client = $this->httpClient() 42 | ->withHeaders($opts->headers) 43 | ->withBody($binary, self::CONTENT_TYPE) 44 | ->when( 45 | filled($username) || filled($password), 46 | fn (PendingRequest $request) => $request->withBasicAuth($username ?? '', $password ?? ''), 47 | ); 48 | 49 | $response = $client->post($adminUrl)->throwIfClientError(); 50 | 51 | return new CupsResponse( 52 | code: $response->status(), 53 | body: $this->interpretResponse($response), 54 | headers: $response->headers(), 55 | opts: $opts, 56 | ); 57 | } 58 | 59 | private function httpClient(): HttpRequest 60 | { 61 | if (! $this->httpClient) { 62 | $this->httpClient = Http::contentType(self::CONTENT_TYPE); 63 | } 64 | 65 | return $this->httpClient; 66 | } 67 | 68 | private function prepareRequest(): array 69 | { 70 | [$username, $password] = $this->getAuth(); 71 | 72 | return [ 73 | $this->getAdminUrl(), 74 | $username, 75 | $password, 76 | ]; 77 | } 78 | 79 | private function interpretResponse(Response $response): string 80 | { 81 | if (! $response->successful()) { 82 | throw new CupsRequestFailed( 83 | code: $response->status(), 84 | ); 85 | } 86 | 87 | return $response->body(); 88 | } 89 | 90 | private function getAdminUrl(): string 91 | { 92 | $scheme = $this->getScheme(); 93 | $ip = $this->getIp(); 94 | $port = $this->getPort(); 95 | 96 | return "{$scheme}://{$ip}:{$port}/admin"; 97 | } 98 | 99 | private function getAuth(): array 100 | { 101 | [$cupsUsername, $cupsPassword] = Cups::getAuth(); 102 | 103 | return [ 104 | $this->username ?? $cupsUsername, 105 | $this->password ?? $cupsPassword, 106 | ]; 107 | } 108 | 109 | private function getIp(): string 110 | { 111 | $myIp = $this->ip ?? Cups::getIp(); 112 | 113 | throw_unless( 114 | filled($myIp), 115 | InvalidRequest::class, 116 | <<<'TXT' 117 | No CUPS IP address provided. (Hint: set your IP address 118 | using "Cups::setIp()") 119 | TXT 120 | ); 121 | 122 | return $myIp; 123 | } 124 | 125 | private function getPort(): int 126 | { 127 | $myPort = $this->port ?? Cups::getPort(); 128 | 129 | throw_unless( 130 | filled($myPort) && is_int($myPort) && $myPort > 0, 131 | InvalidRequest::class, 132 | <<<'TXT' 133 | A positive integer must be used for the CUPS server port. (Hint: 134 | set your port using "Cups::setPort()") 135 | TXT 136 | ); 137 | 138 | return $myPort; 139 | } 140 | 141 | private function getScheme(): string 142 | { 143 | $secure = $this->secure ?? Cups::getSecure(); 144 | 145 | return $secure ? 'https' : 'http'; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Api/Cups/Util/RequestOptions.php: -------------------------------------------------------------------------------- 1 | $this->ip, 28 | 'username' => $this->username, 29 | 'password' => $this->redactedPassword(), 30 | 'port' => $this->port, 31 | 'secure' => $this->secure, 32 | 'headers' => $this->headers, 33 | ]; 34 | } 35 | 36 | /** 37 | * Unpacks an options array into a RequestOptions object. 38 | * 39 | * @param bool $strict when true, forbid arbitrary keys in array form 40 | */ 41 | public static function parse(RequestOptions|array|null $options, bool $strict = false): self 42 | { 43 | if ($options instanceof self) { 44 | return clone $options; 45 | } 46 | 47 | if ($options === null) { 48 | return new self(ip: null, username: null, password: null, headers: []); 49 | } 50 | 51 | if (is_array($options)) { 52 | $headers = []; 53 | $ip = null; 54 | $username = null; 55 | $password = null; 56 | $port = null; 57 | $secure = null; 58 | 59 | if (array_key_exists('ip', $options)) { 60 | $ip = $options['ip']; 61 | unset($options['ip']); 62 | } 63 | 64 | if (array_key_exists('username', $options)) { 65 | $username = $options['username']; 66 | unset($options['username']); 67 | } 68 | 69 | if (array_key_exists('password', $options)) { 70 | $password = $options['password']; 71 | unset($options['password']); 72 | } 73 | 74 | if (array_key_exists('port', $options)) { 75 | $port = $options['port']; 76 | unset($options['port']); 77 | } 78 | 79 | if (array_key_exists('secure', $options)) { 80 | $secure = $options['secure']; 81 | unset($options['secure']); 82 | } 83 | 84 | if ($strict && ! empty($options)) { 85 | $message = 'Got unexpected keys in options array: ' . implode(', ', array_keys($options)); 86 | 87 | throw new InvalidArgument($message); 88 | } 89 | 90 | return new self( 91 | ip: $ip, 92 | username: $username, 93 | password: $password, 94 | port: $port, 95 | secure: $secure, 96 | headers: $headers, 97 | ); 98 | } 99 | 100 | throw new InvalidArgument('Unexpected value received for cups request options.'); 101 | } 102 | 103 | /** 104 | * Unpacks an options array and merges it into the existing RequestOptions object. 105 | * 106 | * @param bool $strict when true, forbid arbitrary keys in array form 107 | */ 108 | public function merge(RequestOptions|array|null $options, bool $strict = false): self 109 | { 110 | $otherOptions = self::parse($options, $strict); 111 | if ($otherOptions->ip === null) { 112 | $otherOptions->ip = $this->ip; 113 | } 114 | 115 | if ($otherOptions->username === null) { 116 | $otherOptions->username = $this->username; 117 | } 118 | 119 | if ($otherOptions->password === null) { 120 | $otherOptions->password = $this->password; 121 | } 122 | 123 | if ($otherOptions->port === Cups::DEFAULT_PORT) { 124 | $otherOptions->port = $this->port; 125 | } 126 | 127 | if ($otherOptions->secure === Cups::DEFAULT_SECURE) { 128 | $otherOptions->secure = $this->secure; 129 | } 130 | 131 | $otherOptions->headers = array_merge($this->headers, $otherOptions->headers); 132 | 133 | return $otherOptions; 134 | } 135 | 136 | private function redactedPassword(): ?string 137 | { 138 | if ($this->password === null) { 139 | return null; 140 | } 141 | 142 | return Str::mask($this->password, '*', 0); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Printing.php: -------------------------------------------------------------------------------- 1 | printer($this->defaultPrinterId); 45 | } 46 | 47 | public function defaultPrinterId(): mixed 48 | { 49 | return $this->defaultPrinterId; 50 | } 51 | 52 | /** 53 | * Use a specific driver on a single call. 54 | */ 55 | public function driver(null|string|PrintDriver $driver = null): static 56 | { 57 | $this->temporaryDriver = app(Factory::class)->driver($driver); 58 | 59 | return $this; 60 | } 61 | 62 | public function getDriver(): Driver 63 | { 64 | return $this->getActiveDriver(); 65 | } 66 | 67 | public function newPrintTask(): Contracts\PrintTask 68 | { 69 | return $this->executeDriverCall( 70 | fn (Driver $driver): Contracts\PrintTask => $driver->newPrintTask(), 71 | ); 72 | } 73 | 74 | public function printer($printerId = null, ...$args): ?Printer 75 | { 76 | return $this->executeDriverCall( 77 | fn (Driver $driver): ?Printer => $driver->printer($printerId, ...$args), 78 | ); 79 | } 80 | 81 | /** 82 | * @return \Illuminate\Support\Collection 83 | */ 84 | public function printers(?int $limit = null, ?int $offset = null, ?string $dir = null, ...$args): Collection 85 | { 86 | return $this->executeDriverCall( 87 | fn (Driver $driver): ?Collection => $driver->printers($limit, $offset, $dir, ...$args), 88 | ) ?? collect(); 89 | } 90 | 91 | /** 92 | * @return \Illuminate\Support\Collection 93 | */ 94 | public function printJobs(?int $limit = null, ?int $offset = null, ?string $dir = null, ...$args): Collection 95 | { 96 | return $this->executeDriverCall( 97 | fn (Driver $driver): ?Collection => $driver->printJobs($limit, $offset, $dir, ...$args), 98 | ) ?? collect(); 99 | } 100 | 101 | public function printJob($jobId = null, ...$args): ?PrintJob 102 | { 103 | return $this->executeDriverCall( 104 | fn (Driver $driver): ?PrintJob => $driver->printJob($jobId, ...$args), 105 | ); 106 | } 107 | 108 | /** 109 | * @return \Illuminate\Support\Collection 110 | */ 111 | public function printerPrintJobs($printerId, ?int $limit = null, ?int $offset = null, ?string $dir = null, ...$args): Collection 112 | { 113 | return $this->executeDriverCall( 114 | fn (Driver $driver): ?Collection => $driver->printerPrintJobs($printerId, $limit, $offset, $dir, ...$args), 115 | ) ?? collect(); 116 | } 117 | 118 | public function printerPrintJob($printerId, $jobId, ...$args): ?PrintJob 119 | { 120 | return $this->executeDriverCall( 121 | fn (Driver $driver): ?PrintJob => $driver->printerPrintJob($printerId, $jobId, ...$args), 122 | ); 123 | } 124 | 125 | protected function executeDriverCall(Closure $callback): mixed 126 | { 127 | try { 128 | return $callback($this->getActiveDriver()); 129 | } catch (Throwable $e) { 130 | static::getLogger()?->error($e->getMessage()); 131 | 132 | return null; 133 | } finally { 134 | // Ensure the driver resets after a single call. 135 | $this->temporaryDriver = null; 136 | } 137 | } 138 | 139 | protected function getActiveDriver(): Driver 140 | { 141 | return $this->temporaryDriver ?? $this->driver; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Api/Cups/Enums/PrinterStateReason.php: -------------------------------------------------------------------------------- 1 | pendingJob = PendingPrintJob::make(); 29 | } 30 | 31 | public function content($content, string|ContentType $contentType = ContentType::RawBase64): static 32 | { 33 | parent::content($content); 34 | 35 | $this->pendingJob 36 | ->setContent($content) 37 | ->setContentType($contentType); 38 | 39 | return $this; 40 | } 41 | 42 | public function file(string $filePath): static 43 | { 44 | $this->pendingJob->addPdfFile($filePath); 45 | 46 | return $this; 47 | } 48 | 49 | public function url(string $url, bool $raw = false): static 50 | { 51 | $this->pendingJob 52 | ->setUrl($url) 53 | ->setContentType($raw ? ContentType::RawUri : ContentType::PdfUri); 54 | 55 | return $this; 56 | } 57 | 58 | public function option(BackedEnum|string $key, $value): static 59 | { 60 | $this->pendingJob->setOption($key, $value); 61 | 62 | return $this; 63 | } 64 | 65 | public function range($start, $end = null): static 66 | { 67 | $range = $start; 68 | 69 | if (! $end && (! Str::contains($range, [',', '-']))) { 70 | $range = "{$range}-"; // print all pages starting from $start 71 | } elseif ($end) { 72 | $range = "{$start}-{$end}"; 73 | } 74 | 75 | return $this->option(PrintJobOption::Pages, $range); 76 | } 77 | 78 | public function tray($tray): static 79 | { 80 | return $this->option(PrintJobOption::Bin, $tray); 81 | } 82 | 83 | public function copies(int $copies): static 84 | { 85 | return $this->option(PrintJobOption::Copies, $copies); 86 | } 87 | 88 | // region PrintNode specific setters 89 | public function contentType(string|ContentType $contentType): static 90 | { 91 | $this->pendingJob->setContentType($contentType); 92 | 93 | return $this; 94 | } 95 | 96 | public function fitToPage(bool $condition): static 97 | { 98 | return $this->option(PrintJobOption::FitToPage, $condition); 99 | } 100 | 101 | public function paper(string $paper): static 102 | { 103 | return $this->option(PrintJobOption::Paper, $paper); 104 | } 105 | 106 | public function expireAfter(int $expireAfter): static 107 | { 108 | $this->pendingJob->setExpireAfter($expireAfter); 109 | 110 | return $this; 111 | } 112 | 113 | public function printQty(int $qty): static 114 | { 115 | $this->pendingJob->setQty($qty); 116 | 117 | return $this; 118 | } 119 | 120 | public function withAuth( 121 | string $username, 122 | #[SensitiveParameter] ?string $password, 123 | string|AuthenticationType $authenticationType = AuthenticationType::Basic, 124 | ): static { 125 | $this->pendingJob->setAuth($username, $password, $authenticationType); 126 | 127 | return $this; 128 | } 129 | // endregion 130 | 131 | public function send(null|array|RequestOptions $opts = null): PrintJobContract 132 | { 133 | $this->ensureValidJob(); 134 | 135 | $this->pendingJob 136 | ->setPrinter($this->printerId) 137 | ->setTitle($this->resolveJobTitle()) 138 | ->setSource($this->printSource); 139 | 140 | $printJob = $this->client->printJobs->create($this->pendingJob, $opts); 141 | 142 | return new PrintJobContract($printJob); 143 | } 144 | 145 | protected function ensureValidJob(): void 146 | { 147 | throw_unless( 148 | filled($this->printerId), 149 | PrintTaskFailed::missingPrinterId(), 150 | ); 151 | 152 | throw_unless( 153 | filled($this->printSource), 154 | PrintTaskFailed::missingSource(), 155 | ); 156 | 157 | throw_unless( 158 | filled($this->pendingJob->contentType), 159 | PrintTaskFailed::missingContentType(), 160 | ); 161 | 162 | throw_unless( 163 | filled($this->pendingJob->content), 164 | PrintTaskFailed::noContent(), 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Service/PrinterService.php: -------------------------------------------------------------------------------- 1 | the max number of rows that will be returned - default is 100 20 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 21 | * `after` => retrieve records with an ID after the provided value 22 | * @return Collection 23 | */ 24 | public function all(?array $params = null, null|array|RequestOptions $opts = null): Collection 25 | { 26 | return $this->requestCollection('get', '/printers', $params, opts: $opts, expectedResource: Printer::class); 27 | } 28 | 29 | public function retrieve(int $id, ?array $params = null, null|array|RequestOptions $opts = null): ?Printer 30 | { 31 | $printers = $this->requestCollection('get', $this->buildPath('/printers/%s', $id), $params, opts: $opts, expectedResource: Printer::class); 32 | 33 | return $printers->first(); 34 | } 35 | 36 | /** 37 | * Retrieve a specific set of printers. 38 | * 39 | * @param array $ids the IDs of the printers to retrieve 40 | * @param null|array $params 41 | * `limit` => the max number of rows that will be returned - default is 100 42 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 43 | * `after` => retrieve records with an ID after the provided value 44 | * @return Collection 45 | */ 46 | public function retrieveSet(array $ids, ?array $params = null, null|array|RequestOptions $opts = null): Collection 47 | { 48 | throw_unless( 49 | filled($ids), 50 | InvalidArgument::class, 51 | 'At least one printer ID must be provided for this request.', 52 | ); 53 | 54 | return $this->requestCollection('get', $this->buildPath('/printers/%s', ...$ids), $params, opts: $opts, expectedResource: Printer::class); 55 | } 56 | 57 | /** 58 | * Retrieve all print jobs associated with a given printer. 59 | * 60 | * @param int|array $parentId the printer's ID; pass an array to retrieve print jobs for multiple printers 61 | * @param null|array $params 62 | * `limit` => the max number of rows that will be returned - default is 100 63 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 64 | * `after` => retrieve records with an ID after the provided value 65 | * @return Collection 66 | */ 67 | public function printJobs(int|array $parentId, ?array $params = null, null|array|RequestOptions $opts = null): Collection 68 | { 69 | $path = is_array($parentId) 70 | ? $this->buildPath('/printers/%s/printjobs', ...$parentId) 71 | : $this->buildPath('/printers/%s/printjobs', $parentId); 72 | 73 | return $this->requestCollection('get', $path, $params, opts: $opts, expectedResource: PrintJob::class); 74 | } 75 | 76 | /** 77 | * Retrieve a single or set of print jobs associated with a given printer. 78 | * 79 | * @param int|array $parentId the printer's ID; pass an array to retrieve print jobs for multiple printers 80 | * @param int|array $printJobId the print job's ID; pass an array to retrieve a set of print jobs 81 | * @param null|array $params 82 | * `limit` => the max number of rows that will be returned - default is 100 83 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 84 | * `after` => retrieve records with an ID after the provided value 85 | * @return Collection|PrintJob|null 86 | */ 87 | public function printJob(int|array $parentId, int|array $printJobId, ?array $params = null, null|array|RequestOptions $opts = null): Collection|PrintJob|null 88 | { 89 | $printerPath = is_array($parentId) 90 | ? $this->buildPath('/printers/%s', ...$parentId) 91 | : $this->buildPath('/printers/%s', $parentId); 92 | 93 | $jobPath = is_array($printJobId) 94 | ? $this->buildPath('/printjobs/%s', ...$printJobId) 95 | : $this->buildPath('/printjobs/%s', $printJobId); 96 | 97 | $response = $this->requestCollection('get', $printerPath . $jobPath, $params, opts: $opts, expectedResource: PrintJob::class); 98 | 99 | return is_array($printJobId) ? $response : $response->first(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Receipts/ReceiptPrinter.php: -------------------------------------------------------------------------------- 1 | connector = new DummyPrintConnector; 57 | $this->printer = new Printer($this->connector); 58 | 59 | static::$lineCharacterLength = config('printing.receipts.line_character_length', 45); 60 | } 61 | 62 | public function __destruct() 63 | { 64 | $this->close(); 65 | } 66 | 67 | public function __toString(): string 68 | { 69 | return $this->connector->getData(); 70 | } 71 | 72 | public function __call($method, $parameters) 73 | { 74 | if (method_exists($this->printer, $method)) { 75 | $this->printer->{$method}(...$parameters); 76 | 77 | return $this; 78 | } 79 | 80 | return $this->__macroCall($method, $parameters); 81 | } 82 | 83 | public function centerAlign(): self 84 | { 85 | $this->printer->setJustification(Printer::JUSTIFY_CENTER); 86 | 87 | return $this; 88 | } 89 | 90 | public function leftAlign(): self 91 | { 92 | $this->printer->setJustification(Printer::JUSTIFY_LEFT); 93 | 94 | return $this; 95 | } 96 | 97 | public function rightAlign(): self 98 | { 99 | $this->printer->setJustification(Printer::JUSTIFY_RIGHT); 100 | 101 | return $this; 102 | } 103 | 104 | public function leftMargin(int $margin = 0): self 105 | { 106 | $this->printer->setPrintLeftMargin($margin); 107 | 108 | return $this; 109 | } 110 | 111 | public function lineHeight(?int $height = null): self 112 | { 113 | $this->printer->setLineSpacing($height); 114 | 115 | return $this; 116 | } 117 | 118 | public function text(string $text, bool $insertNewLine = true): self 119 | { 120 | if ($insertNewLine && ! Str::endsWith($text, "\n")) { 121 | $text = "{$text}\n"; 122 | } 123 | 124 | $this->printer->text($text); 125 | 126 | return $this; 127 | } 128 | 129 | public function twoColumnText(string $left, string $right): self 130 | { 131 | $remaining = static::$lineCharacterLength - strlen($left) - strlen($right); 132 | 133 | if ($remaining <= 0) { 134 | $remaining = 1; 135 | } 136 | 137 | return $this->text($left . str_repeat(' ', $remaining) . $right); 138 | } 139 | 140 | public function barcode($barcodeContent, int $type = Printer::BARCODE_CODE39): self 141 | { 142 | $this->printer->setBarcodeWidth(config('printing.receipts.barcode_width', 2)); 143 | $this->printer->setBarcodeHeight(config('printing.receipts.barcode_height', 64)); 144 | $this->printer->barcode($barcodeContent, $type); 145 | 146 | return $this; 147 | } 148 | 149 | public function line(): self 150 | { 151 | return $this->text(str_repeat('-', static::$lineCharacterLength)); 152 | } 153 | 154 | public function doubleLine(): self 155 | { 156 | return $this->text(str_repeat('=', static::$lineCharacterLength)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Drivers/Cups/PrintTask.php: -------------------------------------------------------------------------------- 1 | pendingJob = PendingPrintJob::make(); 29 | } 30 | 31 | public function content($content, string|ContentType $contentType = ContentType::Pdf): static 32 | { 33 | $this->pendingJob 34 | ->setContent($content) 35 | ->setContentType($contentType); 36 | 37 | return $this; 38 | } 39 | 40 | public function file(string $filePath, string|ContentType $contentType = ContentType::Pdf): static 41 | { 42 | $this->pendingJob->addFile($filePath, $contentType); 43 | 44 | return $this; 45 | } 46 | 47 | public function url(string $url): static 48 | { 49 | parent::url($url); 50 | 51 | $this->pendingJob->setContent($this->content); 52 | 53 | return $this; 54 | } 55 | 56 | public function option(BackedEnum|string $key, $value): static 57 | { 58 | $this->pendingJob->setOption($key, $value); 59 | 60 | return $this; 61 | } 62 | 63 | public function copies(int $copies): static 64 | { 65 | $this->pendingJob->setOption( 66 | OperationAttribute::Copies, 67 | OperationAttribute::Copies->toType($copies), 68 | ); 69 | 70 | return $this; 71 | } 72 | 73 | public function range($start, $end = null): static 74 | { 75 | $this->pendingJob->range($start, $end); 76 | 77 | return $this; 78 | } 79 | 80 | // region Cups specific setters 81 | public function contentType(string|ContentType $contentType): static 82 | { 83 | $this->pendingJob->setContentType($contentType); 84 | 85 | return $this; 86 | } 87 | 88 | public function orientation(string|Orientation $value): static 89 | { 90 | $enum = $value instanceof Orientation 91 | ? $value 92 | : match ($value) { 93 | 'reverse-portrait' => Orientation::ReversePortrait, 94 | 'reverse-landscape' => Orientation::ReverseLandscape, 95 | 'landscape' => Orientation::Landscape, 96 | default => Orientation::Portrait, 97 | }; 98 | 99 | $this->pendingJob->setOption( 100 | OperationAttribute::OrientationRequested, 101 | OperationAttribute::OrientationRequested->toType($enum->value), 102 | ); 103 | 104 | return $this; 105 | } 106 | 107 | public function sides(string|Side $value): static 108 | { 109 | $enum = is_string($value) 110 | ? Side::tryFrom($value) 111 | : $value; 112 | 113 | if (! $enum instanceof Side) { 114 | throw new InvalidArgument( 115 | 'Invalid side "' . $value . '" for the cups driver. Accepted values are: ' . 116 | implode(', ', array_column(Side::cases(), 'value')), 117 | ); 118 | } 119 | 120 | return $this->option( 121 | OperationAttribute::Sides, 122 | OperationAttribute::Sides->toType($enum->value), 123 | ); 124 | } 125 | 126 | public function user(string $name): static 127 | { 128 | $this->pendingJob->setOption( 129 | OperationAttribute::RequestingUserName, 130 | OperationAttribute::RequestingUserName->toType($name), 131 | ); 132 | 133 | return $this; 134 | } 135 | // endregion 136 | 137 | public function send(array|null|RequestOptions $opts = null): PrintJobContract 138 | { 139 | $this->ensureValidJob(); 140 | 141 | $this->pendingJob 142 | ->setPrinter($this->printerId) 143 | ->setTitle($this->resolveJobTitle()) 144 | ->setSource($this->printSource); 145 | 146 | $printJob = $this->client->printJobs->create($this->pendingJob, $opts); 147 | 148 | return new PrintJobContract($printJob); 149 | } 150 | 151 | protected function ensureValidJob(): void 152 | { 153 | throw_unless( 154 | filled($this->printerId), 155 | PrintTaskFailed::missingPrinterId(), 156 | ); 157 | 158 | throw_unless( 159 | filled($this->printSource), 160 | PrintTaskFailed::missingSource(), 161 | ); 162 | 163 | throw_unless( 164 | filled($this->pendingJob->contentType), 165 | PrintTaskFailed::missingContentType(), 166 | ); 167 | 168 | throw_unless( 169 | filled($this->pendingJob->content), 170 | PrintTaskFailed::noContent(), 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Api/Cups/PendingPrintJob.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | public array $options = []; 37 | 38 | /** The uri (id) of the printer to send the job to. */ 39 | public string $printerUri; 40 | 41 | /** A description of the origin of the print job. */ 42 | public string $source = ''; 43 | 44 | /** The title (name) for the new print job. */ 45 | public string $title = ''; 46 | 47 | public static function make(): static 48 | { 49 | return new static; 50 | } 51 | 52 | public function setContent(string $content): static 53 | { 54 | $this->content = $content; 55 | 56 | return $this; 57 | } 58 | 59 | public function addFile(string $filePath, string|ContentType $contentType = ContentType::Pdf): static 60 | { 61 | throw_unless( 62 | file_exists($filePath), 63 | InvalidSource::fileNotFound($filePath), 64 | ); 65 | 66 | try { 67 | $content = file_get_contents($filePath); 68 | } catch (Throwable) { 69 | throw InvalidSource::cannotOpenFile($filePath); 70 | } 71 | 72 | if (blank($content)) { 73 | Printing::getLogger()?->error("No content retrieved from file: {$filePath}"); 74 | } 75 | 76 | $this->content = $content; 77 | 78 | $this->setContentType($contentType); 79 | 80 | return $this; 81 | } 82 | 83 | public function setContentType(string|ContentType $contentType): static 84 | { 85 | $enum = is_string($contentType) 86 | ? ContentType::tryFrom($contentType) 87 | : $contentType; 88 | 89 | if (! $enum instanceof ContentType) { 90 | throw new InvalidArgument( 91 | 'Invalid content type "' . $contentType . '". Must be one of: ' . implode(', ', array_column(ContentType::cases(), 'value')) 92 | ); 93 | } 94 | 95 | $this->contentType = $enum; 96 | 97 | return $this; 98 | } 99 | 100 | public function setOption(string|OperationAttribute $option, Type $value): static 101 | { 102 | $optionKey = $option instanceof OperationAttribute ? $option->value : $option; 103 | 104 | $this->options[$optionKey] = $value; 105 | 106 | return $this; 107 | } 108 | 109 | public function setPrinter(string|PrinterResource|DriverPrinter $printer): static 110 | { 111 | $this->printerUri = match (true) { 112 | $printer instanceof PrinterResource => $printer->uri, 113 | $printer instanceof DriverPrinter => $printer->id(), 114 | default => $printer, 115 | }; 116 | 117 | return $this; 118 | } 119 | 120 | public function setSource(string $source): static 121 | { 122 | $this->source = $source; 123 | 124 | return $this; 125 | } 126 | 127 | public function setTitle(string $title): static 128 | { 129 | $this->title = $title; 130 | 131 | return $this; 132 | } 133 | 134 | public function range($start, $end = null): static 135 | { 136 | $attr = OperationAttribute::PageRanges; 137 | $key = $attr->value; 138 | $type = $attr->toType([$start, $end]); 139 | 140 | if (! array_key_exists($key, $this->options)) { 141 | $this->options[$key] = $type; 142 | 143 | return $this; 144 | } 145 | 146 | if (! is_array($this->options[$key])) { 147 | $this->options[$key] = [$this->options[$key]]; 148 | } 149 | 150 | $this->options[$key][] = $type; 151 | 152 | return $this; 153 | } 154 | 155 | public function toPendingRequest(): PendingRequest 156 | { 157 | return (new PendingRequest) 158 | ->setVersion(Version::V1_1) 159 | ->setOperation(Operation::PrintJob) 160 | ->addOperationAttributes([ 161 | OperationAttribute::PrinterUri->value => OperationAttribute::PrinterUri->toType($this->printerUri), 162 | OperationAttribute::DocumentFormat->value => OperationAttribute::DocumentFormat->toType($this->contentType->value), 163 | OperationAttribute::JobName->value => OperationAttribute::JobName->toType($this->title), 164 | ]) 165 | ->addJobAttributes($this->options) 166 | ->setContent($this->content); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Api/Cups/BaseCupsClient.php: -------------------------------------------------------------------------------- 1 | null, 21 | 'username' => null, 22 | 'password' => null, 23 | 'port' => Cups::DEFAULT_PORT, 24 | 'secure' => Cups::DEFAULT_SECURE, 25 | ]; 26 | 27 | private array $config; 28 | 29 | private RequestOptions $defaultOpts; 30 | 31 | public function __construct(#[SensitiveParameter] ?array $config = []) 32 | { 33 | $config = array_merge(self::DEFAULT_CONFIG, $config ?? []); 34 | $this->guardAgainstInvalidConfig($config); 35 | 36 | $this->config = $config; 37 | 38 | $this->setDefaultOpts(); 39 | } 40 | 41 | public function getConfig(): array 42 | { 43 | return $this->config; 44 | } 45 | 46 | public function getIp(): ?string 47 | { 48 | return $this->config['ip']; 49 | } 50 | 51 | public function getAuth(): array 52 | { 53 | return [ 54 | $this->config['username'], 55 | $this->config['password'], 56 | ]; 57 | } 58 | 59 | public function getPort(): ?int 60 | { 61 | return $this->config['port']; 62 | } 63 | 64 | public function getSecure(): ?bool 65 | { 66 | return $this->config['secure']; 67 | } 68 | 69 | public function request(string|PendingRequest $binary, array|RequestOptions $opts = []): CupsResponse 70 | { 71 | $defaultRequestOpts = $this->defaultOpts; 72 | 73 | $opts = $defaultRequestOpts->merge($opts, true); 74 | 75 | [$username, $password] = $this->authForRequest($opts); 76 | 77 | $requestor = new CupsRequestor( 78 | ip: $this->ipForRequest($opts), 79 | username: $username, 80 | password: $password, 81 | port: $this->portForRequest($opts), 82 | secure: $this->secureForRequest($opts), 83 | ); 84 | 85 | return $requestor->request( 86 | binary: $binary, 87 | opts: $opts, 88 | ); 89 | } 90 | 91 | private function ipForRequest(RequestOptions $opts): string 92 | { 93 | $ip = $opts->ip ?? $this->getIp() ?? Cups::getIp(); 94 | 95 | throw_if( 96 | blank($ip), 97 | InvalidRequest::class, 98 | <<<'TXT' 99 | No CUPS Server IP address provided. Set your IP when constructing the 100 | CupsClient instance, or provide it on a per-request basis using the 101 | `ip` key in the $opts argument. 102 | TXT 103 | ); 104 | 105 | return $ip; 106 | } 107 | 108 | private function authForRequest(RequestOptions $opts): array 109 | { 110 | [$thisUsername, $thisPassword] = $this->getAuth(); 111 | [$globalUsername, $globalPassword] = Cups::getAuth(); 112 | 113 | $username = $opts->username ?? $thisUsername ?? $globalUsername; 114 | $password = $opts->password ?? $thisPassword ?? $globalPassword; 115 | 116 | return [$username, $password]; 117 | } 118 | 119 | private function portForRequest(RequestOptions $opts): int 120 | { 121 | $port = $opts->port ?? $this->getPort() ?? Cups::getPort(); 122 | 123 | throw_if( 124 | $port < 1, 125 | InvalidRequest::class, 126 | 'Invalid server port: ' . $port, 127 | ); 128 | 129 | return $port; 130 | } 131 | 132 | private function secureForRequest(RequestOptions $opts): bool 133 | { 134 | return $opts->secure ?? $this->getSecure() ?? Cups::getSecure(); 135 | } 136 | 137 | private function setDefaultOpts(): void 138 | { 139 | [$username, $password] = Cups::getAuth(); 140 | 141 | $this->defaultOpts = RequestOptions::parse([ 142 | 'ip' => Cups::getIp(), 143 | 'username' => $username, 144 | 'password' => $password, 145 | 'port' => Cups::getPort(), 146 | 'secure' => Cups::getSecure(), 147 | ]); 148 | } 149 | 150 | private function guardAgainstInvalidConfig(#[SensitiveParameter] array $config): void 151 | { 152 | // IP Address 153 | throw_if( 154 | $config['ip'] !== null && ! is_string($config['ip']), 155 | InvalidArgumentException::class, 156 | 'cups server ip must be null or a string', 157 | ); 158 | 159 | throw_if( 160 | $config['ip'] !== null && ($config['ip'] === ''), 161 | InvalidArgumentException::class, 162 | 'cups server ip cannot be an empty string', 163 | 164 | ); 165 | 166 | throw_if( 167 | $config['ip'] !== null && (preg_match('/\s/', $config['ip'])), 168 | InvalidArgumentException::class, 169 | 'cups server ip cannot contain whitespace', 170 | ); 171 | 172 | // Check absence of extra keys 173 | $extraConfigKeys = array_diff(array_keys($config), array_keys(self::DEFAULT_CONFIG)); 174 | throw_if( 175 | filled($extraConfigKeys), 176 | InvalidArgumentException::class, 177 | 'Found unknown key(s) in configuration array: ' . "'" . implode("', '", $extraConfigKeys) . "'", 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Api/PrintNode/Service/ComputerService.php: -------------------------------------------------------------------------------- 1 | the max number of rows that will be returned - default is 100 18 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 19 | * `after` => retrieve records with an ID after the provided value 20 | * @return Collection 21 | */ 22 | public function all(?array $params = null, null|array|RequestOptions $opts = null): Collection 23 | { 24 | return $this->requestCollection('get', '/computers', $params, opts: $opts, expectedResource: Computer::class); 25 | } 26 | 27 | public function retrieve(int $id, ?array $params = null, null|array|RequestOptions $opts = null): ?Computer 28 | { 29 | $computers = $this->requestCollection('get', $this->buildPath('/computers/%s', $id), $params, opts: $opts, expectedResource: Computer::class); 30 | 31 | return $computers->first(); 32 | } 33 | 34 | /** 35 | * Retrieve a specific set of computers. 36 | * 37 | * @param array $ids the IDs of the computers to retrieve 38 | * @param null|array $params 39 | * `limit` => the max number of rows that will be returned - default is 100 40 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 41 | * `after` => retrieve records with an ID after the provided value 42 | * @return Collection 43 | */ 44 | public function retrieveSet(array $ids, ?array $params = null, null|array|RequestOptions $opts = null): Collection 45 | { 46 | throw_unless( 47 | filled($ids), 48 | InvalidArgument::class, 49 | 'At least one computer ID must be provided for this request.', 50 | ); 51 | 52 | return $this->requestCollection('get', $this->buildPath('/computers/%s', ...$ids), $params, opts: $opts, expectedResource: Computer::class); 53 | } 54 | 55 | /** 56 | * Delete a given computer. Returns an array of affected IDs. 57 | */ 58 | public function delete(int $id, ?array $params = null, null|array|RequestOptions $opts = null): array 59 | { 60 | return $this->request('delete', $this->buildPath('/computers/%s', $id), $params, opts: $opts); 61 | } 62 | 63 | /** 64 | * Delete a set of computers. Omit or use an empty array of $ids to delete all computers. 65 | * Returns an array of affected IDs. 66 | */ 67 | public function deleteMany(array $ids = [], ?array $params = null, null|array|RequestOptions $opts = null): array 68 | { 69 | $path = filled($ids) 70 | ? $this->buildPath('/computers/%s', ...$ids) 71 | : '/computers'; 72 | 73 | return $this->request('delete', $path, $params, opts: $opts); 74 | } 75 | 76 | /** 77 | * Retrieve all printers attached to a given computer. 78 | * 79 | * @param int|array $parentId the computer's ID; pass an array to retrieve printers for multiple computers 80 | * @param null|array $params 81 | * `limit` => the max number of rows that will be returned - default is 100 82 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 83 | * `after` => retrieve records with an ID after the provided value 84 | * @return Collection 85 | */ 86 | public function printers(int|array $parentId, ?array $params = null, null|array|RequestOptions $opts = null): Collection 87 | { 88 | $path = is_array($parentId) 89 | ? $this->buildPath('/computers/%s/printers', ...$parentId) 90 | : $this->buildPath('/computers/%s/printers', $parentId); 91 | 92 | return $this->requestCollection('get', $path, $params, opts: $opts, expectedResource: Printer::class); 93 | } 94 | 95 | /** 96 | * Retrieve a set of printers attached to a given computer. 97 | * 98 | * @param array|int $parentId the computer's ID; pass an array to retrieve a printer for multiple computers 99 | * @param array|int $printerId the printer's ID; pass an array to retrieve a set of printers 100 | * @param null|array $params 101 | * `limit` => the max number of rows that will be returned - default is 100 102 | * `dir` => `asc` for ascending, `desc` for descending - default is `desc` 103 | * `after` => retrieve records with an ID after the provided value 104 | * @return Collection|Printer|null 105 | */ 106 | public function printer(int|array $parentId, int|array $printerId, ?array $params = null, null|array|RequestOptions $opts = null): Collection|Printer|null 107 | { 108 | $computerPath = is_array($parentId) 109 | ? $this->buildPath('/computers/%s', ...$parentId) 110 | : $this->buildPath('/computers/%s', $parentId); 111 | 112 | $printerPath = is_array($printerId) 113 | ? $this->buildPath('/printers/%s', ...$printerId) 114 | : $this->buildPath('/printers/%s', $printerId); 115 | 116 | $response = $this->requestCollection('get', $computerPath . $printerPath, $params, opts: $opts, expectedResource: Printer::class); 117 | 118 | return is_array($printerId) ? $response : $response->first(); 119 | } 120 | } 121 | --------------------------------------------------------------------------------