├── .empty ├── test ├── system │ ├── TransloaditRequest │ │ ├── Base.php │ │ ├── TransloaditRequestNoJsonErrorTest.php │ │ ├── TransloaditRequestErrorTest.php │ │ ├── TransloaditRequestHttpsRootTest.php │ │ ├── TransloaditRequestRootTest.php │ │ ├── TransloaditRequestGetBillTest.php │ │ └── TransloaditRequestAssemblyCreateTest.php │ ├── CurlRequest │ │ └── CurlRequestRootTest.php │ └── Transloadit │ │ ├── TransloaditAssemblyCreateTest.php │ │ └── TransloaditCreateAssemblyWaitForCompletionTest.php ├── fixture │ └── image-resize-robot.jpg ├── config.php ├── bootstrap.php └── simple │ ├── CurlResponseTest.php │ ├── TransloaditResponseTest.php │ ├── CurlRequestTest.php │ ├── TransloaditRequestTest.php │ └── TransloaditTest.php ├── .dockerignore ├── examples ├── fixture │ └── straw-apple.jpg ├── common │ └── loader.php ├── 4-fetch-assembly-status.php ├── 5-assembly-with-template.php ├── 7-disable-ssl-verification.php ├── 1-assembly-request.php ├── 6-assembly-with-timeout.php ├── 2-assembly-form.php └── 3-assembly-form-with-jquery-plugin.php ├── .gitignore ├── .coveralls.yml ├── .vscode └── php-sdk.code-workspace ├── composer.json ├── Dockerfile ├── lib └── transloadit │ ├── CurlResponse.php │ ├── TransloaditResponse.php │ ├── TransloaditRequest.php │ ├── CurlRequest.php │ └── Transloadit.php ├── phpunit.xml ├── LICENSE ├── tool └── generate-example-docs.php ├── Makefile ├── scripts ├── test-in-docker.sh └── notify-registry.sh ├── .github └── workflows │ └── ci.yml ├── CONTRIBUTING.md ├── phpcs.xml ├── CHANGELOG.md └── README.md /.empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/TransloaditRequest/Base.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .docker-cache 4 | .env 5 | vendor 6 | node_modules 7 | transloadit-*.tgz 8 | -------------------------------------------------------------------------------- /examples/fixture/straw-apple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transloadit/php-sdk/HEAD/examples/fixture/straw-apple.jpg -------------------------------------------------------------------------------- /test/fixture/image-resize-robot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transloadit/php-sdk/HEAD/test/fixture/image-resize-robot.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Readme.html 2 | 3 | composer.phar 4 | build/ 5 | vendor/ 6 | bin/composer 7 | env.sh 8 | .phpunit.cache 9 | .aider* 10 | .env 11 | .docker-cache/ 12 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | # https://coveralls.io/r/transloadit/php-sdk 3 | # https://github.com/satooshi/php-coveralls 4 | src_dir: lib/transloadit 5 | coverage_clover: build/logs/clover.xml 6 | -------------------------------------------------------------------------------- /.vscode/php-sdk.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ], 7 | "settings": { 8 | "workbench.colorCustomizations": { 9 | "titleBar.activeForeground": "#232531", 10 | "titleBar.activeBackground": "#8993be" 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/common/loader.php: -------------------------------------------------------------------------------- 1 | request->url = 'http://google.com/'; 8 | $response = $this->request->execute(); 9 | 10 | $error = $response->error(); 11 | $this->assertStringContainsString('no json', $error); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/system/CurlRequest/CurlRequestRootTest.php: -------------------------------------------------------------------------------- 1 | url = 'http://api2.transloadit.com/'; 11 | $request->method = 'GET'; 12 | $response = $request->execute(); 13 | $this->assertStringContainsString('"ok"', $response->data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/4-fetch-assembly-status.php: -------------------------------------------------------------------------------- 1 | Assembly Status. 9 | */ 10 | $assemblyId = 'MY_ASSEMBLY_ID'; 11 | 12 | $transloadit = new Transloadit([ 13 | 'key' => 'MY_TRANSLOADIT_KEY', 14 | 'secret' => 'MY_TRANSLOADIT_SECRET', 15 | ]); 16 | 17 | $response = $transloadit->getAssembly($assemblyId); 18 | 19 | echo '
';
20 | print_r($response);
21 | echo '
'; 22 | -------------------------------------------------------------------------------- /test/system/TransloaditRequest/TransloaditRequestErrorTest.php: -------------------------------------------------------------------------------- 1 | request->setMethodAndPath('POST', '/assemblies'); 8 | $response = $this->request->execute(); 9 | 10 | $error = $response->error(); 11 | $this->assertStringContainsString('transloadit', $error); 12 | $this->assertStringContainsString('STEPS', $error); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/system/TransloaditRequest/TransloaditRequestHttpsRootTest.php: -------------------------------------------------------------------------------- 1 | request->protocol = 'https'; 8 | $this->request->setMethodAndPath('GET', '/'); 9 | $response = $this->request->execute(); 10 | 11 | $this->assertEquals(true, array_key_exists('ok', $response->data)); 12 | $this->assertInstanceOf('transloadit\TransloaditResponse', $response); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/system/TransloaditRequest/TransloaditRequestRootTest.php: -------------------------------------------------------------------------------- 1 | request->setMethodAndPath('GET', '/'); 8 | $response = $this->request->execute(); 9 | 10 | $this->assertStringStartsWith('Transloadit-Client: php-sdk:', $this->request->headers[1]); 11 | $this->assertEquals(true, isset($response->data['ok'])); 12 | $this->assertInstanceOf('transloadit\TransloaditResponse', $response); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/config.php: -------------------------------------------------------------------------------- 1 | request->setMethodAndPath('GET', '/bill/' . date('Y-m')); 8 | $response = $this->request->execute(); 9 | 10 | if (isset($response->data['ok'])) { 11 | $this->assertStringContainsString('BILL', $response->data['ok']); 12 | return; 13 | } 14 | 15 | $this->assertArrayHasKey( 16 | 'error', 17 | $response->data, 18 | 'Bill response should include ok or error field' 19 | ); 20 | $this->assertStringContainsString('BILL', (string) $response->data['error']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transloadit/php-sdk", 3 | "type": "library", 4 | "description": "Transloadit SDK", 5 | "keywords": [ 6 | "transloadit", 7 | "video", 8 | "encoding", 9 | "thumbnails", 10 | "image", 11 | "resizing", 12 | "audio", 13 | "waveform", 14 | "document", 15 | "processing", 16 | "file", 17 | "uploading" 18 | ], 19 | "homepage": "https://github.com/transloadit/php-sdk", 20 | "license": "MIT", 21 | "version": "3.3.0", 22 | "require": { 23 | "php": ">=7.4.0" 24 | }, 25 | "require-dev": { 26 | "php-coveralls/php-coveralls": "^2.5", 27 | "phpunit/phpunit": "^9.5", 28 | "squizlabs/php_codesniffer": "^3.7" 29 | }, 30 | "autoload": { 31 | "psr-0": { 32 | "transloadit": "lib" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/system/TransloaditRequest/TransloaditRequestAssemblyCreateTest.php: -------------------------------------------------------------------------------- 1 | request->setMethodAndPath('POST', '/assemblies'); 8 | $this->request->files[] = TEST_FIXTURE_DIR . '/image-resize-robot.jpg'; 9 | $this->request->params = [ 10 | 'steps' => [ 11 | 'resize' => [ 12 | 'robot' => '/image/resize', 13 | 'width' => 100, 14 | 'height' => 100, 15 | 'result' => true, 16 | ], 17 | ], 18 | ]; 19 | $response = $this->request->execute(); 20 | 21 | $this->assertEquals('ASSEMBLY_EXECUTING', $response->data['ok']); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM php:8.3-cli AS base 4 | 5 | ENV COMPOSER_ALLOW_SUPERUSER=1 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y --no-install-recommends \ 9 | git \ 10 | unzip \ 11 | zip \ 12 | libzip-dev \ 13 | curl \ 14 | ca-certificates \ 15 | && docker-php-ext-configure zip \ 16 | && docker-php-ext-install zip \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | COPY --from=composer:2 /usr/bin/composer /usr/bin/composer 20 | 21 | # Install Node.js (for transloadit CLI parity checks) 22 | RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ 23 | && apt-get install -y --no-install-recommends nodejs \ 24 | && npm install -g npm@latest \ 25 | && rm -rf /var/lib/apt/lists/* 26 | 27 | WORKDIR /workspace 28 | -------------------------------------------------------------------------------- /examples/5-assembly-with-template.php: -------------------------------------------------------------------------------- 1 | Assembly 8 | with Templates. 9 | 10 | You are expected to create a Template on your Transloadit account dashboard 11 | and add the Template ID here. 12 | */ 13 | 14 | use transloadit\Transloadit; 15 | 16 | $transloadit = new Transloadit([ 17 | 'key' => 'MY_TRANSLOADIT_KEY', 18 | 'secret' => 'MY_TRANSLOADIT_SECRET', 19 | ]); 20 | 21 | $response = $transloadit->createAssembly([ 22 | 'files' => [dirname(__FILE__) . '/fixture/straw-apple.jpg'], 23 | 'params' => [ 24 | 'template_id' => 'MY_TEMPLATE_ID', 25 | ], 26 | ]); 27 | 28 | // Show the results of the assembly we spawned 29 | echo '
';
30 | print_r($response);
31 | echo '
'; 32 | -------------------------------------------------------------------------------- /lib/transloadit/CurlResponse.php: -------------------------------------------------------------------------------- 1 | $val) { 15 | $this->{$key} = $val; 16 | } 17 | } 18 | 19 | public function parseJson() { 20 | $decoded = json_decode($this->data, true); 21 | if (!is_array($decoded)) { 22 | return false; 23 | } 24 | 25 | $this->data = $decoded; 26 | return true; 27 | } 28 | 29 | public function error() { 30 | if (!$this->curlErrorNumber) { 31 | return false; 32 | } 33 | 34 | return sprintf( 35 | 'curl: %d: %s', 36 | $this->curlErrorNumber, 37 | $this->curlErrorMessage 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/7-disable-ssl-verification.php: -------------------------------------------------------------------------------- 1 | getenv('MY_TRANSLOADIT_KEY'), 13 | 'secret' => getenv('MY_TRANSLOADIT_SECRET'), 14 | ]); 15 | 16 | $response = $transloadit->createAssembly([ 17 | 'files' => [dirname(__FILE__) . '/fixture/straw-apple.jpg'], 18 | 'curlOptions' => [ 19 | CURLOPT_SSL_VERIFYHOST => 0, 20 | CURLOPT_SSL_VERIFYPEER => 0, 21 | ], 22 | 'waitForCompletion' => true, 23 | 'params' => [ 24 | 'steps' => [ 25 | 'resize' => [ 26 | 'robot' => '/image/resize', 27 | 'width' => 200, 28 | 'height' => 100, 29 | ], 30 | ], 31 | ], 32 | ]); 33 | 34 | // Show the results of the assembly we spawned 35 | echo ''; 36 | print_r($response); 37 | echo ''; 38 | -------------------------------------------------------------------------------- /lib/transloadit/TransloaditResponse.php: -------------------------------------------------------------------------------- 1 | data)) { 17 | return sprintf('transloadit: bad response, no json: %s', $this->data); 18 | } 19 | 20 | if (array_key_exists('error', $this->data)) { 21 | $error = sprintf('transloadit: %s', $this->data['error']); 22 | 23 | if (array_key_exists('message', $this->data)) { 24 | $error .= sprintf(': %s', $this->data['message']); 25 | } 26 | 27 | if (array_key_exists('reason', $this->data)) { 28 | $error .= sprintf(': %s', $this->data['reason']); 29 | } 30 | 31 | return $error; 32 | } 33 | 34 | if (!array_key_exists('ok', $this->data)) { 35 | return 'transloadit: bad response data, no ok / error key included.'; 36 | } 37 | 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | test 20 | 21 | 22 | 23 | 25 | 26 | lib 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 Transloadit 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tool/generate-example-docs.php: -------------------------------------------------------------------------------- 1 | Readme.html 45 | -------------------------------------------------------------------------------- /examples/1-assembly-request.php: -------------------------------------------------------------------------------- 1 | Assembly 8 | on your server. 9 | 10 | It takes a sample image file, uploads it to Transloadit, and starts a 11 | resizing job on it. 12 | */ 13 | 14 | use transloadit\Transloadit; 15 | 16 | $transloadit = new Transloadit([ 17 | 'key' => 'MY_TRANSLOADIT_KEY', 18 | 'secret' => 'MY_TRANSLOADIT_SECRET', 19 | ]); 20 | 21 | $response = $transloadit->createAssembly([ 22 | // Use dirname(__FILE__) to get the current directory, then append the relative path to the image 23 | // You can replace this with an absolute path to any file on your server that PHP can access 24 | 'files' => [dirname(__FILE__) . '/fixture/straw-apple.jpg'], 25 | 'params' => [ 26 | 'steps' => [ 27 | 'resize' => [ 28 | 'robot' => '/image/resize', 29 | 'width' => 200, 30 | 'height' => 100, 31 | ], 32 | ], 33 | ], 34 | ]); 35 | 36 | // Show the results of the assembly we spawned 37 | echo '
';
38 | print_r($response);
39 | echo '
'; 40 | -------------------------------------------------------------------------------- /test/system/Transloadit/TransloaditAssemblyCreateTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped( 13 | 'TRANSLOADIT_KEY and TRANSLOADIT_SECRET environment variables are required.' 14 | ); 15 | return; 16 | } 17 | 18 | $this->transloadit = new Transloadit([ 19 | 'key' => getenv('TRANSLOADIT_KEY'), 20 | 'secret' => getenv('TRANSLOADIT_SECRET'), 21 | ]); 22 | } 23 | 24 | public function testRoot() { 25 | $response = $this->transloadit->createAssembly([ 26 | 'files' => [TEST_FIXTURE_DIR . '/image-resize-robot.jpg'], 27 | 'params' => [ 28 | 'steps' => [ 29 | 'resize' => [ 30 | 'robot' => '/image/resize', 31 | 'width' => 100, 32 | 'height' => 100, 33 | 'result' => true, 34 | ], 35 | ], 36 | ] 37 | ]); 38 | $this->assertEquals('ASSEMBLY_EXECUTING', $response->data['ok']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/6-assembly-with-timeout.php: -------------------------------------------------------------------------------- 1 | getenv('MY_TRANSLOADIT_KEY'), 9 | 'secret' => getenv('MY_TRANSLOADIT_SECRET'), 10 | ]); 11 | 12 | $response = $transloadit->createAssembly([ 13 | 'files' => [dirname(__FILE__) . '/fixture/straw-apple.jpg'], 14 | 'curlOptions' => [ 15 | CURLOPT_TIMEOUT_MS => 1, 16 | // We can't finish in the specified: '1ms' so we expect this example 17 | // to fail with: $response->curlErrorNumber === 28 18 | // 19 | // You can pass any curl option here that your PHP/curl version supports: 20 | // https://www.php.net/manual/en/function.curl-setopt.php 21 | // Note that if you are interested in timeouts, perhaps also consider 22 | // that you can set waitForCompletion to false and use the 23 | // notify_url feature to get a webhook pingback when the Assembly is done. 24 | ], 25 | 'params' => [ 26 | 'steps' => [ 27 | 'resize' => [ 28 | 'robot' => '/image/resize', 29 | 'width' => 200, 30 | 'height' => 100, 31 | ], 32 | ], 33 | ], 34 | ]); 35 | 36 | // Show the results of the assembly we spawned 37 | echo ''; 38 | print_r([ 39 | 'errcode' => $response->curlErrorNumber, 40 | 'errmsg' => $response->curlErrorMessage, 41 | ]); 42 | echo ''; 43 | -------------------------------------------------------------------------------- /test/system/Transloadit/TransloaditCreateAssemblyWaitForCompletionTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped( 13 | 'Have a look at test/config.php to get this test to run.' 14 | ); 15 | return; 16 | } 17 | 18 | // @todo Load config from git excluded config file 19 | $this->transloadit = new Transloadit([ 20 | 'key' => TRANSLOADIT_KEY, 21 | 'secret' => TRANSLOADIT_SECRET, 22 | ]); 23 | } 24 | public function testRoot() { 25 | $response = $this->transloadit->createAssembly([ 26 | 'files' => [TEST_FIXTURE_DIR . '/image-resize-robot.jpg'], 27 | 'params' => [ 28 | 'steps' => [ 29 | 'resize' => [ 30 | 'robot' => '/image/resize', 31 | 'width' => 100, 32 | 'height' => 100, 33 | 'result' => true, 34 | ], 35 | ], 36 | ], 37 | 'waitForCompletion' => true 38 | ]); 39 | $this->assertEquals('ASSEMBLY_COMPLETED', $response->data['ok']); 40 | 41 | $getResp = $this->transloadit->getAssembly($response->data['assembly_id']); 42 | $this->assertEquals('ASSEMBLY_COMPLETED', $getResp->data['ok']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | markTestSkipped( 19 | 'Have a look at test/config.php to get this test to run.' 20 | ); 21 | return; 22 | } 23 | 24 | $this->request = new \transloadit\TransloaditRequest([ 25 | 'key' => TRANSLOADIT_KEY, 26 | 'secret' => TRANSLOADIT_SECRET, 27 | ]); 28 | 29 | try { 30 | $nonce = bin2hex(random_bytes(16)); 31 | } catch (\Exception $e) { 32 | $nonce = uniqid('php-sdk-', true); 33 | } 34 | $this->request->params['auth']['nonce'] = $nonce; 35 | } 36 | } 37 | 38 | class TransloaditRequestTestCase extends \PHPUnit\Framework\TestCase { 39 | protected \transloadit\Transloadit $transloadit; 40 | 41 | public function setUp(): void { 42 | if (!defined('TRANSLOADIT_KEY') || !defined('TRANSLOADIT_SECRET')) { 43 | $this->markTestSkipped( 44 | 'Have a look at test/config.php to get this test to run.' 45 | ); 46 | return; 47 | } 48 | 49 | $this->transloadit = new \transloadit\Transloadit([ 50 | 'key' => TRANSLOADIT_KEY, 51 | 'secret' => TRANSLOADIT_SECRET, 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/simple/CurlResponseTest.php: -------------------------------------------------------------------------------- 1 | response = new CurlResponse(); 12 | } 13 | 14 | public function testAttributes() { 15 | $this->assertEquals(null, $this->response->data); 16 | $this->assertEquals(null, $this->response->curlErrorNumber); 17 | $this->assertEquals(null, $this->response->curlErrorMessage); 18 | $this->assertEquals(null, $this->response->curlInfo); 19 | } 20 | 21 | public function testConstructor() { 22 | $transloadit = new CurlResponse(['data' => 'foobar']); 23 | $this->assertEquals('foobar', $transloadit->data); 24 | } 25 | 26 | public function testParseJson() { 27 | $data = ['foo' => 'bar']; 28 | 29 | $this->response->data = json_encode($data); 30 | $r = $this->response->parseJson(); 31 | 32 | $this->assertEquals(true, $r); 33 | $this->assertEquals($data, $this->response->data); 34 | 35 | $data = $this->response->data = 'no json'; 36 | $r = $this->response->parseJson(); 37 | 38 | $this->assertEquals(false, $r); 39 | $this->assertEquals($data, $this->response->data); 40 | } 41 | 42 | public function testError() { 43 | $error = $this->response->error(); 44 | $this->assertEquals(false, $error); 45 | 46 | $number = $this->response->curlErrorNumber = 27; 47 | $message = $this->response->curlErrorMessage = 'Something went wrong'; 48 | $error = $this->response->error(); 49 | $this->assertEquals(sprintf('curl: %d: %s', $number, $message), $error); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/2-assembly-form.php: -------------------------------------------------------------------------------- 1 | Assembly is shown using `Transloadit::response()`. 12 | 13 |
14 | There is no guarantee that the Assembly has already finished 15 | executing by the time the `$response` is fetched. You should use 16 | the `notify_url` parameter for this. 17 |
18 | */ 19 | 20 | use transloadit\Transloadit; 21 | 22 | $transloadit = new Transloadit([ 23 | 'key' => 'MY_TRANSLOADIT_KEY', 24 | 'secret' => 'MY_TRANSLOADIT_SECRET', 25 | ]); 26 | 27 | // Check if this request is a Transloadit redirect_url notification. 28 | // If so fetch the response and output the current assembly status: 29 | $response = Transloadit::response(); 30 | if ($response) { 31 | echo '

Assembly Status:

'; 32 | echo '
';
33 |   print_r($response);
34 |   echo '
'; 35 | exit; 36 | } 37 | 38 | // This should work on most environments, but you might have to modify 39 | // this for your particular setup. 40 | $redirectUrl = sprintf('http://%s%s', $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']); 41 | 42 | // Setup a simple file upload form that resizes an image to 200x100px 43 | echo $transloadit->createAssemblyForm([ 44 | 'params' => [ 45 | 'steps' => [ 46 | 'resize' => [ 47 | 'robot' => '/image/resize', 48 | 'width' => 200, 49 | 'height' => 100, 50 | ], 51 | ], 52 | 'redirect_url' => $redirectUrl, 53 | ], 54 | ]); 55 | ?> 56 |

Pick an image to resize

57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /test/simple/TransloaditResponseTest.php: -------------------------------------------------------------------------------- 1 | response = new TransloaditResponse(); 13 | } 14 | 15 | public function testConstructor() { 16 | $this->assertInstanceOf('transloadit\\CurlResponse', $this->response); 17 | } 18 | 19 | public function testError() { 20 | $this->response->data = 'no json'; 21 | $error = $this->response->error(); 22 | $this->assertEquals( 23 | sprintf('transloadit: bad response, no json: ' . $this->response->data), 24 | $error 25 | ); 26 | 27 | $this->response->data = ['ok' => 'ASSEMBLY_DOING_SOMETHING']; 28 | $error = $this->response->error(); 29 | $this->assertEquals(false, $error); 30 | 31 | unset($this->response->data['ok']); 32 | $error = $this->response->error(); 33 | $this->assertEquals( 34 | sprintf('transloadit: bad response data, no ok / error key included.'), 35 | $error 36 | ); 37 | 38 | $ERROR = 'ASSEMBLY_WENT_TOTALLY_BAD'; 39 | $this->response->data['error'] = $ERROR; 40 | $error = $this->response->error(); 41 | $this->assertEquals( 42 | sprintf('transloadit: %s', $ERROR), 43 | $error 44 | ); 45 | 46 | $MESSAGE = 'Something went awefully wrong!'; 47 | $this->response->data['message'] = $MESSAGE; 48 | $error = $this->response->error(); 49 | $this->assertEquals( 50 | sprintf('transloadit: %s: %s', $ERROR, $MESSAGE), 51 | $error 52 | ); 53 | 54 | $REASON = 'Something went awefully wrong!'; 55 | $this->response->data['reason'] = $REASON; 56 | $error = $this->response->error(); 57 | $this->assertEquals( 58 | sprintf('transloadit: %s: %s: %s', $ERROR, $MESSAGE, $REASON), 59 | $error 60 | ); 61 | 62 | $this->response->curlErrorNumber = 27; 63 | $error = $this->response->error(); 64 | $this->assertStringContainsString('curl', $error); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/3-assembly-form-with-jquery-plugin.php: -------------------------------------------------------------------------------- 1 | 'MY_TRANSLOADIT_KEY', 17 | 'secret' => 'MY_TRANSLOADIT_SECRET', 18 | ]); 19 | 20 | $response = Transloadit::response(); 21 | if ($response) { 22 | echo '

Assembly Status:

'; 23 | echo '
';
24 |   print_r($response);
25 |   echo '
'; 26 | exit; 27 | } 28 | 29 | $redirectUrl = sprintf('http://%s%s', $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']); 30 | 31 | echo $transloadit->createAssemblyForm([ 32 | 'params' => [ 33 | 'steps' => [ 34 | 'resize' => [ 35 | 'robot' => '/image/resize', 36 | 'width' => 200, 37 | 'height' => 100, 38 | ], 39 | ], 40 | 'redirect_url' => $redirectUrl, 41 | ], 42 | ]); 43 | ?> 44 | 48 | 49 | 53 | 59 | 60 |

Pick an image to resize

61 |
62 | 63 | 64 |
65 | -------------------------------------------------------------------------------- /scripts/test-in-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | IMAGE_NAME=${IMAGE_NAME:-transloadit-php-sdk-dev} 5 | CACHE_DIR=.docker-cache 6 | 7 | ensure_docker() { 8 | if ! command -v docker >/dev/null 2>&1; then 9 | echo "Docker is required to run this script." >&2 10 | exit 1 11 | fi 12 | 13 | if ! docker info >/dev/null 2>&1; then 14 | if [[ -z "${DOCKER_HOST:-}" && -S "$HOME/.colima/default/docker.sock" ]]; then 15 | export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" 16 | fi 17 | fi 18 | 19 | if ! docker info >/dev/null 2>&1; then 20 | echo "Docker daemon is not reachable. Start Docker (or Colima) and retry." >&2 21 | exit 1 22 | fi 23 | } 24 | 25 | configure_platform() { 26 | if [[ -z "${DOCKER_PLATFORM:-}" ]]; then 27 | local arch 28 | arch=$(uname -m) 29 | if [[ "$arch" == "arm64" || "$arch" == "aarch64" ]]; then 30 | DOCKER_PLATFORM=linux/amd64 31 | fi 32 | fi 33 | } 34 | 35 | ensure_docker 36 | configure_platform 37 | 38 | if [[ $# -eq 0 ]]; then 39 | RUN_CMD='set -e; composer install --no-interaction --prefer-dist; make test-all' 40 | else 41 | printf -v USER_CMD '%q ' "$@" 42 | RUN_CMD="set -e; composer install --no-interaction --prefer-dist; ${USER_CMD}" 43 | fi 44 | 45 | mkdir -p "$CACHE_DIR/composer-cache" "$CACHE_DIR/npm-cache" "$CACHE_DIR/composer-home" 46 | 47 | BUILD_ARGS=() 48 | if [[ -n "${DOCKER_PLATFORM:-}" ]]; then 49 | BUILD_ARGS+=(--platform "$DOCKER_PLATFORM") 50 | fi 51 | BUILD_ARGS+=(-t "$IMAGE_NAME" -f Dockerfile .) 52 | 53 | docker build "${BUILD_ARGS[@]}" 54 | 55 | DOCKER_ARGS=( 56 | --rm 57 | --user "$(id -u):$(id -g)" 58 | -e HOME=/workspace 59 | -e COMPOSER_HOME=/workspace/$CACHE_DIR/composer-home 60 | -e COMPOSER_CACHE_DIR=/workspace/$CACHE_DIR/composer-cache 61 | -e npm_config_cache=/workspace/$CACHE_DIR/npm-cache 62 | -e TEST_NODE_PARITY="${TEST_NODE_PARITY:-0}" 63 | -v "$PWD":/workspace 64 | -w /workspace 65 | ) 66 | 67 | if [[ -n "${DOCKER_PLATFORM:-}" ]]; then 68 | DOCKER_ARGS+=(--platform "$DOCKER_PLATFORM") 69 | fi 70 | 71 | if [[ -f .env ]]; then 72 | DOCKER_ARGS+=(--env-file "$PWD/.env") 73 | fi 74 | 75 | exec docker run "${DOCKER_ARGS[@]}" "$IMAGE_NAME" bash -lc "$RUN_CMD" 76 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | ci: 12 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: true 16 | max-parallel: 1 17 | matrix: 18 | php: 19 | - 8.1 20 | - 8.2 21 | dependencies: 22 | - locked 23 | - lowest 24 | - highest 25 | name: PHP ${{ matrix.php }} - ${{ matrix.dependencies }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 1 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: '20' 33 | - name: Install tsx 34 | run: npm install -g tsx 35 | - uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | tools: php-cs-fixer, phpunit 39 | coverage: ${{ matrix.php == '8.1' && matrix.dependencies == 'locked' && 'xdebug' || 'none' }} 40 | - uses: ramsey/composer-install@v3 41 | with: 42 | dependency-versions: ${{ matrix.dependencies }} 43 | composer-options: '--ignore-platform-reqs' 44 | - name: Test with Coverage 45 | if: matrix.php == '8.1' && matrix.dependencies == 'locked' 46 | run: | 47 | make test-all-coverage 48 | env: 49 | TRANSLOADIT_KEY: ${{secrets.TEST_ACCOUNT_KEY}} 50 | TRANSLOADIT_SECRET: ${{secrets.TEST_ACCOUNT_SECRET}} 51 | TEST_NODE_PARITY: 1 52 | - name: Test without Coverage 53 | if: matrix.php != '8.1' || matrix.dependencies != 'locked' 54 | run: | 55 | make test-all 56 | env: 57 | TRANSLOADIT_KEY: ${{secrets.TEST_ACCOUNT_KEY}} 58 | TRANSLOADIT_SECRET: ${{secrets.TEST_ACCOUNT_SECRET}} 59 | TEST_NODE_PARITY: 1 60 | - name: Publish Coverage Report 61 | if: github.event_name == 'pull_request' && matrix.php == '8.1' && matrix.dependencies == 'locked' 62 | uses: lucassabreu/comment-coverage-clover@v0.13.0 63 | with: 64 | file: ./build/logs/clover.xml 65 | with-table: true 66 | with-chart: false 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Feel free to fork this project. We will happily merge bug fixes or other small 4 | improvements. For bigger changes you should probably get in touch with us 5 | before you start to avoid not seeing them merged. 6 | 7 | ## Testing 8 | 9 | ### Basic Tests 10 | 11 | ```bash 12 | make test 13 | ``` 14 | 15 | ### System Tests 16 | 17 | System tests require: 18 | 19 | 1. Valid Transloadit credentials in environment: 20 | 21 | ```bash 22 | export TRANSLOADIT_KEY='your-auth-key' 23 | export TRANSLOADIT_SECRET='your-auth-secret' 24 | ``` 25 | 26 | Then run: 27 | 28 | ```bash 29 | make test-all 30 | ``` 31 | 32 | ### Node.js Reference Implementation Parity Assertions 33 | 34 | The SDK includes assertions that compare Smart CDN URL signatures and regular request signatures with our reference Node.js implementation. To run these tests: 35 | 36 | 1. Requirements: 37 | 38 | - Node.js 20+ with npm 39 | - Ability to execute `npx transloadit smart_sig` (the CLI is downloaded on demand) 40 | - Ability to execute `npx transloadit sig` (the CLI is downloaded on demand) 41 | 42 | 2. Run the tests: 43 | 44 | ```bash 45 | export TRANSLOADIT_KEY='your-auth-key' 46 | export TRANSLOADIT_SECRET='your-auth-secret' 47 | TEST_NODE_PARITY=1 make test-all 48 | ``` 49 | 50 | If you want to warm the CLI cache ahead of time you can run: 51 | 52 | ```bash 53 | npx --yes transloadit smart_sig --help 54 | ``` 55 | 56 | For regular request signatures, you can also prime the CLI by running: 57 | 58 | ```bash 59 | TRANSLOADIT_KEY=... TRANSLOADIT_SECRET=... \ 60 | npx --yes transloadit sig --algorithm sha1 --help 61 | ``` 62 | 63 | CI opts into `TEST_NODE_PARITY=1`, and you can optionally do this locally as well. 64 | 65 | ### Run Tests in Docker 66 | 67 | Use `scripts/test-in-docker.sh` for a reproducible environment: 68 | 69 | ```bash 70 | ./scripts/test-in-docker.sh 71 | ``` 72 | 73 | This builds the local image, runs `composer install`, and executes `make test-all` (unit + integration tests). Pass a custom command to run something else (composer install still runs first): 74 | 75 | ```bash 76 | ./scripts/test-in-docker.sh vendor/bin/phpunit --filter signedSmartCDNUrl 77 | ``` 78 | 79 | Environment variables such as `TEST_NODE_PARITY` or the credentials in `.env` are forwarded, so you can combine parity checks and integration tests with Docker: 80 | 81 | ```bash 82 | TEST_NODE_PARITY=1 ./scripts/test-in-docker.sh 83 | ``` 84 | 85 | ## Releasing a new version 86 | 87 | To release, say `3.3.0` [Packagist](https://packagist.org/packages/transloadit/php-sdk), follow these steps: 88 | 89 | 1. Make sure `PACKAGIST_TOKEN` is set in your `.env` file 90 | 1. Make sure you are in main: `git checkout main` 91 | 1. Make sure `CHANGELOG.md` and `composer.json` have been updated 92 | 1. Commit: `git add CHANGELOG.md composer.json && git commit -m "Release v3.3.0"` 93 | 1. Tag: `git tag v3.3.0` 94 | 1. Push: `git push --tags` 95 | 1. Notify Packagist (runs via Docker): `VERSION=3.3.0 ./scripts/notify-registry.sh` 96 | 1. Publish a GitHub release (include the changelog). This triggers the release workflow. (via the GitHub UI, `gh release creates v3.3.0 --title "v3.3.0" --notes-file <(cat CHANGELOG.md section)`) 97 | 98 | The notify script reuses the same Docker image as `./scripts/test-in-docker.sh`, so Docker is the only requirement on your workstation. 99 | 100 | This project implements the [Semantic Versioning](http://semver.org/) guidelines. 101 | -------------------------------------------------------------------------------- /lib/transloadit/TransloaditRequest.php: -------------------------------------------------------------------------------- 1 | method = $method; 25 | $this->path = $path; 26 | } 27 | 28 | public function getParamsString() { 29 | $params = $this->params; 30 | if (!isset($params['auth'])) { 31 | $params['auth'] = []; 32 | } 33 | 34 | if (!ini_get('date.timezone')) { 35 | date_default_timezone_set('Etc/UTC'); 36 | } 37 | 38 | $params['auth'] = $params['auth'] + [ 39 | 'key' => $this->key, 40 | 'expires' => gmdate('Y/m/d H:i:s+00:00', strtotime($this->expires)), 41 | ]; 42 | return json_encode($params); 43 | } 44 | 45 | public function signString($string) { 46 | if (empty($this->secret)) { 47 | return null; 48 | } 49 | 50 | return hash_hmac('sha1', $string, $this->secret); 51 | } 52 | 53 | public function prepare() { 54 | $params = $this->getParamsString(); 55 | $this->fields['params'] = $params; 56 | 57 | $signature = $this->signString($params); 58 | if ($signature) { 59 | $this->fields['signature'] = $signature; 60 | } 61 | 62 | $this->configureUrl(); 63 | } 64 | 65 | public function configureUrl() { 66 | if (!empty($this->url)) { 67 | return; 68 | } 69 | 70 | $this->url = sprintf( 71 | '%s%s', 72 | $this->endpoint, 73 | $this->path 74 | ); 75 | } 76 | 77 | public function execute($response = null) { 78 | // note: $response is not used here, only needed to keep PHP strict mode 79 | // happy. 80 | 81 | $this->prepare(); 82 | $response = parent::execute(new TransloaditResponse()); 83 | $response->parseJson(); 84 | 85 | if ($this->path === '/assemblies' && $this->waitForCompletion) { 86 | return $this->_waitForCompletion($response); 87 | } 88 | return $response; 89 | } 90 | 91 | private function _waitForCompletion($response) { 92 | // Try assembly_ssl_url first, fall back to assembly_url 93 | $assemblyUrl = $response->data['assembly_ssl_url'] ?? $response->data['assembly_url'] ?? null; 94 | if (!$assemblyUrl) { 95 | throw new \RuntimeException('No assembly URL found in response. Response data: ' . json_encode($response->data)); 96 | } 97 | 98 | $parts = parse_url($assemblyUrl); 99 | 100 | while (true) { 101 | $req = new TransloaditRequest(); 102 | $req->endpoint = 'https://' . $parts['host']; 103 | $req->path = $parts['path']; 104 | $req->curlOptions = $this->curlOptions; 105 | $response = $req->execute(); 106 | 107 | if (isset($response->data['ok'])) { 108 | if ($response->data['ok'] === 'ASSEMBLY_UPLOADING' || $response->data['ok'] === 'ASSEMBLY_EXECUTING') { 109 | sleep(1); 110 | continue; 111 | } 112 | } 113 | 114 | // If this is an unknown, erroneous or completed Assembly completion state, return right away. 115 | return $response; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /scripts/notify-registry.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | set -o errtrace 4 | # set -o xtrace 5 | 6 | IMAGE_NAME=${IMAGE_NAME:-transloadit-php-sdk-dev} 7 | CACHE_DIR=.docker-cache 8 | 9 | ensure_docker() { 10 | if ! command -v docker >/dev/null 2>&1; then 11 | echo "Docker is required to run this script." >&2 12 | exit 1 13 | fi 14 | 15 | if ! docker info >/dev/null 2>&1; then 16 | if [[ -z "${DOCKER_HOST:-}" && -S "$HOME/.colima/default/docker.sock" ]]; then 17 | export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" 18 | fi 19 | fi 20 | 21 | if ! docker info >/dev/null 2>&1; then 22 | echo "Docker daemon is not reachable. Start Docker (or Colima) and retry." >&2 23 | exit 1 24 | fi 25 | } 26 | 27 | configure_platform() { 28 | if [[ -z "${DOCKER_PLATFORM:-}" ]]; then 29 | local arch 30 | arch=$(uname -m) 31 | if [[ "$arch" == "arm64" || "$arch" == "aarch64" ]]; then 32 | DOCKER_PLATFORM=linux/amd64 33 | fi 34 | fi 35 | } 36 | 37 | if [[ "${1:-}" != "--inside-container" ]]; then 38 | ensure_docker 39 | configure_platform 40 | 41 | mkdir -p "$CACHE_DIR/composer-cache" "$CACHE_DIR/npm-cache" "$CACHE_DIR/composer-home" 42 | 43 | BUILD_ARGS=() 44 | if [[ -n "${DOCKER_PLATFORM:-}" ]]; then 45 | BUILD_ARGS+=(--platform "$DOCKER_PLATFORM") 46 | fi 47 | BUILD_ARGS+=(-t "$IMAGE_NAME" -f Dockerfile .) 48 | 49 | docker build "${BUILD_ARGS[@]}" 50 | 51 | DOCKER_ARGS=( 52 | --rm 53 | --user "$(id -u):$(id -g)" 54 | -e HOME=/workspace 55 | -e COMPOSER_HOME=/workspace/$CACHE_DIR/composer-home 56 | -e COMPOSER_CACHE_DIR=/workspace/$CACHE_DIR/composer-cache 57 | -e npm_config_cache=/workspace/$CACHE_DIR/npm-cache 58 | -v "$PWD":/workspace 59 | -w /workspace 60 | ) 61 | 62 | if [[ -n "${DOCKER_PLATFORM:-}" ]]; then 63 | DOCKER_ARGS+=(--platform "$DOCKER_PLATFORM") 64 | fi 65 | 66 | if [[ -f .env ]]; then 67 | DOCKER_ARGS+=(--env-file "$PWD/.env") 68 | fi 69 | 70 | if [[ -n "${PACKAGIST_TOKEN:-}" ]]; then 71 | DOCKER_ARGS+=(-e "PACKAGIST_TOKEN=${PACKAGIST_TOKEN}") 72 | fi 73 | 74 | if [[ -n "${VERSION:-}" ]]; then 75 | DOCKER_ARGS+=(-e "VERSION=${VERSION}") 76 | fi 77 | 78 | exec docker run "${DOCKER_ARGS[@]}" "$IMAGE_NAME" bash -lc "./scripts/notify-registry.sh --inside-container" 79 | fi 80 | 81 | shift 82 | 83 | if [[ -z "${PACKAGIST_TOKEN:-}" ]]; then 84 | if [[ -f .env ]]; then 85 | # shellcheck disable=SC1091 86 | source .env || { 87 | echo "Failed to source .env" 88 | exit 1 89 | } 90 | fi 91 | if [[ -z "${PACKAGIST_TOKEN:-}" ]]; then 92 | echo "PACKAGIST_TOKEN is not set" 93 | exit 1 94 | fi 95 | fi 96 | 97 | if [[ -z "${VERSION:-}" ]]; then 98 | echo "VERSION is not set" 99 | exit 1 100 | fi 101 | 102 | if ! grep "${VERSION}" composer.json > /dev/null 2>&1; then 103 | echo "First add '${VERSION}' to composer.json please" 104 | exit 1 105 | fi 106 | if ! grep "${VERSION}" CHANGELOG.md > /dev/null 2>&1; then 107 | echo "First add '${VERSION}' to CHANGELOG.md please" 108 | exit 1 109 | fi 110 | if [[ -n "$(git status --porcelain)" ]]; then 111 | echo "Git working tree not clean. First commit all your work please." 112 | exit 1 113 | fi 114 | 115 | curl \ 116 | -X POST \ 117 | -H 'Content-Type: application/json' \ 118 | -d '{"repository":{"url":"https://github.com/transloadit/php-sdk"}}' \ 119 | "https://packagist.org/api/update-package?username=kvz&apiToken=${PACKAGIST_TOKEN}" 120 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | plugins/ 5 | vendor/ 6 | vendors/ 7 | webroot/ 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 0 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | */config/* 79 | */tests/* 80 | */src/Command/* 81 | 82 | 83 | 84 | 0 85 | 86 | 87 | -------------------------------------------------------------------------------- /lib/transloadit/CurlRequest.php: -------------------------------------------------------------------------------- 1 | $val) { 17 | $this->{$key} = $val; 18 | } 19 | } 20 | 21 | public function getCurlOptions() { 22 | $url = $this->url; 23 | 24 | $hasBody = ($this->method === 'PUT' || $this->method === 'POST'); 25 | if (!$hasBody) { 26 | $url .= '?' . http_build_query($this->fields); 27 | } 28 | 29 | if (!is_array($this->curlOptions)) { 30 | $this->curlOptions = [$this->curlOptions]; 31 | } 32 | 33 | // Obtain SDK version 34 | if (empty($this->version)) { 35 | $pathToComposerJson = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..'; 36 | $pathToComposerJson .= DIRECTORY_SEPARATOR . 'composer.json'; 37 | $composerData = json_decode(file_get_contents($pathToComposerJson)); 38 | $this->version = $composerData->version; 39 | } 40 | 41 | if (!empty($this->headers)) { 42 | foreach ($this->headers as $key => $value) { 43 | if (strpos($value, 'Transloadit-Client') === 0) { 44 | $this->headers[$key] = sprintf($value, $this->version); 45 | } 46 | } 47 | } 48 | 49 | $options = $this->curlOptions + [ 50 | CURLOPT_RETURNTRANSFER => true, 51 | CURLOPT_CUSTOMREQUEST => $this->method, 52 | CURLOPT_URL => $url, 53 | CURLOPT_HTTPHEADER => $this->headers, 54 | ]; 55 | 56 | if ($hasBody) { 57 | $fields = $this->fields; 58 | foreach ($this->files as $field => $file) { 59 | if (!file_exists($file)) { 60 | trigger_error('File ' . $file . ' does not exist', E_USER_ERROR); 61 | return false; 62 | } 63 | if (is_int($field)) { 64 | $field = 'file_' . ($field + 1); 65 | } 66 | 67 | // -- Start edit -- 68 | // Edit by Aart Berkhout involving issue #8: CURL depricated functions (PHP 5.5) 69 | // https://github.com/transloadit/php-sdk/issues/8 70 | if (function_exists('curl_file_create')) { 71 | // For >= PHP 5.5 use curl_file_create 72 | $fields[$field] = curl_file_create($file); 73 | } else { 74 | // For < PHP 5.5 use @filename API 75 | $fields[$field] = '@' . $file; 76 | } 77 | // -- End edit -- 78 | } 79 | $options[CURLOPT_POSTFIELDS] = $fields; 80 | } 81 | 82 | return $options; 83 | } 84 | 85 | public function execute($response = null) { 86 | $curl = curl_init(); 87 | 88 | // -- Start edit -- 89 | // For PHP 5.6 Safe Upload is required to upload files using curl in PHP 5.5, add the CURLOPT_SAFE_UPLOAD = true option 90 | if (defined('CURLOPT_SAFE_UPLOAD')) { 91 | curl_setopt($curl, CURLOPT_SAFE_UPLOAD, function_exists('curl_file_create') ? true : false); 92 | } 93 | // -- End edit -- 94 | 95 | curl_setopt_array($curl, $this->getCurlOptions()); 96 | 97 | if (!$response) { 98 | $response = new CurlResponse(); 99 | } 100 | $response->data = curl_exec($curl); 101 | $response->curlInfo = curl_getinfo($curl); 102 | $response->curlErrorNumber = curl_errno($curl); 103 | $response->curlErrorMessage = curl_error($curl); 104 | 105 | curl_close($curl); 106 | 107 | return $response; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/simple/CurlRequestTest.php: -------------------------------------------------------------------------------- 1 | request = new CurlRequest(); 12 | } 13 | 14 | public function testAttributes() { 15 | $this->assertEquals('GET', $this->request->method); 16 | $this->assertEquals(null, $this->request->url); 17 | $this->assertEquals([], $this->request->headers); 18 | $this->assertEquals([], $this->request->fields); 19 | $this->assertEquals([], $this->request->files); 20 | } 21 | 22 | public function testConstructor() { 23 | $request = new CurlRequest(['url' => 'foobar']); 24 | $this->assertEquals('foobar', $request->url); 25 | } 26 | 27 | public function testGetCurlOptions() { 28 | // test return transfer 29 | $options = $this->request->getCurlOptions(); 30 | $this->assertEquals(true, $options[CURLOPT_RETURNTRANSFER]); 31 | 32 | // test method 33 | $this->request->method = 'PUT'; 34 | $options = $this->request->getCurlOptions(); 35 | $this->assertEquals($this->request->method, $options[CURLOPT_CUSTOMREQUEST]); 36 | 37 | // test url 38 | $this->request->url = 'http://foo.com/bar'; 39 | $options = $this->request->getCurlOptions(); 40 | $this->assertEquals($this->request->url, $options[CURLOPT_URL]); 41 | 42 | // test headers 43 | $this->request->headers = ['Foo: bar']; 44 | $options = $this->request->getCurlOptions(); 45 | $this->assertEquals($this->request->headers, $options[CURLOPT_HTTPHEADER]); 46 | 47 | // test put fields 48 | $this->request->fields = ['hello' => 'world']; 49 | $options = $this->request->getCurlOptions(); 50 | $this->assertEquals($this->request->fields, $options[CURLOPT_POSTFIELDS]); 51 | 52 | // test post fields 53 | $this->request->method = 'POST'; 54 | $options = $this->request->getCurlOptions(); 55 | $this->assertEquals($this->request->fields, $options[CURLOPT_POSTFIELDS]); 56 | $this->assertEquals($this->request->url, $options[CURLOPT_URL]); 57 | 58 | // test get query 59 | $this->request->method = 'GET'; 60 | $options = $this->request->getCurlOptions(); 61 | $this->assertEquals( 62 | $this->request->url . '?' . http_build_query($this->request->fields), 63 | $options[CURLOPT_URL] 64 | ); 65 | $this->assertArrayNotHasKey(CURLOPT_POSTFIELDS, $options); 66 | 67 | $fixture = dirname(dirname(__FILE__)) . '/fixture/image-resize-robot.jpg'; 68 | 69 | // test post files 70 | $this->request->method = 'POST'; 71 | $this->request->fields = ['super' => 'cool']; 72 | $this->request->files = ['foo' => $fixture]; 73 | $options = $this->request->getCurlOptions(); 74 | 75 | // -- Start edit -- 76 | // Edit by Aart Berkhout involving issue #8: CURL depricated functions (PHP 5.5) 77 | // https://github.com/transloadit/php-sdk/issues/8 78 | $filesOptions = function_exists('curl_file_create') ? 79 | ['foo' => curl_file_create($this->request->files['foo'])] : 80 | ['foo' => '@' . $this->request->files['foo']]; 81 | 82 | $this->assertEquals( 83 | array_merge( 84 | $this->request->fields, 85 | $filesOptions 86 | ), 87 | $options[CURLOPT_POSTFIELDS] 88 | ); 89 | 90 | // test file numbering 91 | $this->request->files = [$fixture]; 92 | $options = $this->request->getCurlOptions(); 93 | 94 | $filesOptions = function_exists('curl_file_create') ? 95 | ['file_1' => curl_file_create($this->request->files[0])] : 96 | ['file_1' => '@' . $this->request->files[0]]; 97 | 98 | $this->assertEquals( 99 | array_merge( 100 | $this->request->fields, 101 | $filesOptions 102 | ), 103 | $options[CURLOPT_POSTFIELDS] 104 | ); 105 | // -- End edit -- 106 | } 107 | 108 | public function testExecute() { 109 | // Can't test this method because PHP doesn't allow stubbing the calls 110 | // to curl easily. However, the method hardly contains any logic as all 111 | // of that is located in other methods. 112 | $this->assertTrue(true); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [main](https://github.com/transloadit/php-sdk/tree/main) 4 | 5 | diff: https://github.com/transloadit/php-sdk/compare/3.3.0...main 6 | 7 | ## [3.3.0](https://github.com/transloadit/php-sdk/tree/3.3.0) 8 | 9 | - Replace the custom Node parity helper with the official `transloadit` CLI for Smart CDN signatures 10 | - Add a Docker-based test harness and document the parity workflow 11 | - Randomize system-test request signatures and document optional `auth.nonce` usage to avoid replay protection failures 12 | 13 | diff: https://github.com/transloadit/php-sdk/compare/3.2.0...3.3.0 14 | 15 | ## [3.2.0](https://github.com/transloadit/php-sdk/tree/3.2.0) 16 | 17 | - Implement `signedSmartCDNUrl` 18 | 19 | diff: https://github.com/transloadit/php-sdk/compare/3.1.0...3.2.0 20 | 21 | ## [3.1.0](https://github.com/transloadit/php-sdk/tree/3.1.0) 22 | 23 | - Pass down `curlOptions` when `TransloaditRequest` reinstantiates itself for `waitForCompletion` 24 | 25 | diff: https://github.com/transloadit/php-sdk/compare/3.0.4-dev...3.1.0 26 | 27 | ## [3.0.4-dev](https://github.com/transloadit/php-sdk/tree/3.0.4-dev) 28 | 29 | - Pass down `curlOptions` when `TransloaditRequest` reinstantiates itself for `waitForCompletion` 30 | 31 | diff: https://github.com/transloadit/php-sdk/compare/3.0.4...3.0.4-dev 32 | 33 | ## [3.0.4](https://github.com/transloadit/php-sdk/tree/3.0.4) 34 | 35 | - Ditch `v` prefix in versions as that's more idiomatic 36 | - Bring back the getAssembly() function 37 | - Implement Transloadit client header. Closes #25. (#28) 38 | - Fix waitForCompletion 39 | - Travis php & ubuntu version changes 40 | - fix: remove deprecation warning 41 | - Rename tl->deleteAssembly to cancelAssembly and add it to the Readme 42 | 43 | diff: https://github.com/transloadit/php-sdk/compare/v2.0.0...3.0.4 44 | 45 | ## [v2.1.0](https://github.com/transloadit/php-sdk/tree/v2.1.0) 46 | 47 | - Fix for CURL deprecated functions (thanks @ABerkhout) 48 | - CI improvements (phpunit, travis, composer) 49 | - Add example for fetching the assembly status 50 | - Add ability to set additional curl_setopt (thanks @michaelkasper) 51 | 52 | diff: https://github.com/transloadit/php-sdk/compare/v2.0.0...v2.1.0 53 | 54 | ## [v2.0.0](https://github.com/transloadit/php-sdk/tree/v2.0.0) 55 | 56 | - Retire host + protocol in favor of one endpoint property, 57 | allow passing that on to the Request object. 58 | - Improve readme (getting started) 59 | - Don't rely on globally installed phpunit when we can ship it via Composer 60 | 61 | diff: https://github.com/transloadit/php-sdk/compare/v1.0.1...v2.0.0 62 | 63 | ## [v1.0.1](https://github.com/transloadit/php-sdk/tree/v1.0.1) 64 | 65 | - Fix broken examples 66 | - Improve documentation (version changelogs) 67 | 68 | diff: https://github.com/transloadit/php-sdk/compare/v1.0.0...v1.0.1 69 | 70 | ## [v1.0.0](https://github.com/transloadit/php-sdk/tree/v1.0.0) 71 | 72 | A big thanks to [@nervetattoo](https://github.com/nervetattoo) for making this version happen! 73 | 74 | - Add support for Composer 75 | - Make phpunit run through Composer 76 | - Change to namespaced PHP 77 | 78 | diff: https://github.com/transloadit/php-sdk/compare/v0.10.0...v1.0.0 79 | 80 | ## [v0.10.0](https://github.com/transloadit/php-sdk/tree/v0.10.0) 81 | 82 | - Add support for Strict mode 83 | - Add support for more auth params 84 | - Improve documentation 85 | - Bug fixes 86 | - Refactoring 87 | 88 | diff: https://github.com/transloadit/php-sdk/compare/v0.9.1...v0.10.0 89 | 90 | ## [v0.9.1](https://github.com/transloadit/php-sdk/tree/v0.9.1) 91 | 92 | - Improve documentation 93 | - Better handling of errors & non-json responses 94 | - Change directory layout 95 | 96 | diff: https://github.com/transloadit/php-sdk/compare/v0.9...v0.9.1 97 | 98 | ## [v0.9](https://github.com/transloadit/php-sdk/tree/v0.9) 99 | 100 | - Use markdown for docs 101 | - Add support for signed GET requests 102 | - Add support for HTTPS 103 | - Simplified API 104 | - Improve handling of magic quotes 105 | 106 | diff: https://github.com/transloadit/php-sdk/compare/v0.2...v0.9 107 | 108 | ## [v0.2](https://github.com/transloadit/php-sdk/tree/v0.2) 109 | 110 | - Add error handling 111 | 112 | diff: https://github.com/transloadit/php-sdk/compare/v0.1...v0.2 113 | 114 | ## [v0.1](https://github.com/transloadit/php-sdk/tree/v0.1) 115 | 116 | The very first version 117 | -------------------------------------------------------------------------------- /lib/transloadit/Transloadit.php: -------------------------------------------------------------------------------- 1 | $val) { 12 | $this->{$key} = $val; 13 | } 14 | } 15 | 16 | public function request($options = [], $execute = true) { 17 | $options = $options + [ 18 | 'key' => $this->key, 19 | 'secret' => $this->secret, 20 | 'endpoint' => $this->endpoint, 21 | 'waitForCompletion' => false, 22 | ]; 23 | $request = new TransloaditRequest($options); 24 | return ($execute) 25 | ? $request->execute() 26 | : $request; 27 | } 28 | 29 | public static function response() { 30 | if (!empty($_POST['transloadit'])) { 31 | $json = $_POST['transloadit']; 32 | if (ini_get('magic_quotes_gpc') === '1') { 33 | $json = stripslashes($json); 34 | } 35 | 36 | $response = new TransloaditResponse(); 37 | $response->data = json_decode($json, true); 38 | return $response; 39 | } 40 | 41 | if (!empty($_GET['assembly_url'])) { 42 | $request = new TransloaditRequest([ 43 | 'url' => $_GET['assembly_url'], 44 | ]); 45 | return $request->execute(); 46 | } 47 | return false; 48 | } 49 | 50 | public function createAssemblyForm($options = []) { 51 | $out = []; 52 | 53 | $customFormAttributes = []; 54 | if (array_key_exists('attributes', $options)) { 55 | $customFormAttributes = $options['attributes']; 56 | unset($options['attributes']); 57 | } 58 | 59 | $assembly = $this->request($options + [ 60 | 'method' => 'POST', 61 | 'path' => '/assemblies', 62 | ], false); 63 | $assembly->prepare(); 64 | 65 | $formAttributes = [ 66 | 'action' => $assembly->url, 67 | 'method' => $assembly->method, 68 | 'enctype' => 'multipart/form-data', 69 | ] + $customFormAttributes; 70 | 71 | $formAttributeList = []; 72 | foreach ($formAttributes as $key => $val) { 73 | $formAttributeList[] = sprintf('%s="%s"', $key, htmlentities($val)); 74 | } 75 | 76 | $out[] = '
'; 77 | 78 | foreach ($assembly->fields as $field => $val) { 79 | $out[] = sprintf( 80 | '', 81 | 'hidden', 82 | $field, 83 | htmlentities($val) 84 | ); 85 | } 86 | 87 | return join("\n", $out); 88 | } 89 | 90 | public function createAssembly($options) { 91 | return $this->request($options + [ 92 | 'method' => 'POST', 93 | 'path' => '/assemblies', 94 | ]); 95 | } 96 | 97 | // Leave this in for BC. 98 | public function getAssembly($assembly_id) { 99 | $response = $this->request([ 100 | 'method' => 'GET', 101 | 'path' => '/assemblies/' . $assembly_id, 102 | ], true); 103 | 104 | return $response; 105 | } 106 | 107 | public function deleteAssembly($assembly_id) { 108 | return $this->cancelAssembly($assembly_id); 109 | } 110 | 111 | public function cancelAssembly($assembly_id) { 112 | // Look up the host for this assembly 113 | $response = $this->request([ 114 | 'method' => 'GET', 115 | 'path' => '/assemblies/' . $assembly_id, 116 | ], true); 117 | 118 | $error = $response->error(); 119 | if ($error) { 120 | return $error; 121 | } 122 | 123 | $url = parse_url($response->data['assembly_url']); 124 | 125 | $response = $this->request([ 126 | 'method' => 'DELETE', 127 | 'path' => $url['path'], 128 | 'host' => $url['host'], 129 | ]); 130 | 131 | $error = $response->error(); 132 | if ($error) { 133 | return $error; 134 | } else { 135 | return $response; 136 | } 137 | } 138 | 139 | /** 140 | * Generates a signed URL for Transloadit's Smart CDN 141 | * https://transloadit.com/services/content-delivery/ 142 | * 143 | * @param string $workspaceSlug The workspace slug 144 | * @param string $templateSlug The template slug 145 | * @param string $inputField The input field (optional) 146 | * @param array $params Additional parameters (optional) 147 | * @param int $expireAtMs Number of milliseconds since epoch at which the URL expires 148 | * @return string The signed URL 149 | */ 150 | public function signedSmartCDNUrl( 151 | string $workspaceSlug, 152 | string $templateSlug, 153 | string $inputField = '', 154 | array $params = [], 155 | int $expireAtMs = null 156 | ): string { 157 | // Validate required fields 158 | if (!$workspaceSlug) { 159 | throw new \InvalidArgumentException('workspace is required'); 160 | } 161 | if (!$templateSlug) { 162 | throw new \InvalidArgumentException('template is required'); 163 | } 164 | if ($inputField === null) { 165 | throw new \InvalidArgumentException('input must be a string'); 166 | } 167 | 168 | // Add auth parameters 169 | $queryParams = []; 170 | 171 | // Process params to match Node.js behavior 172 | foreach ($params as $key => $value) { 173 | if (is_array($value)) { 174 | foreach ($value as $val) { 175 | if ($val !== null) { 176 | $queryParams[$key][] = $val; 177 | } 178 | } 179 | } elseif ($value !== null) { 180 | $queryParams[$key] = $value; 181 | } 182 | } 183 | 184 | $queryParams['auth_key'] = $this->key; 185 | $queryParams['exp'] = (string)($expireAtMs ?? (time() * 1000 + 3600000)); // Default 1 hour 186 | 187 | // Sort parameters alphabetically 188 | ksort($queryParams); 189 | 190 | // Build query string manually to match Node.js behavior 191 | $queryParts = []; 192 | foreach ($queryParams as $key => $value) { 193 | if (is_array($value)) { 194 | foreach ($value as $val) { 195 | $queryParts[] = rawurlencode($key) . '=' . rawurlencode($val); 196 | } 197 | } else { 198 | $queryParts[] = rawurlencode($key) . '=' . rawurlencode($value); 199 | } 200 | } 201 | $queryString = implode('&', $queryParts); 202 | 203 | // Build the string to sign 204 | $stringToSign = sprintf( 205 | '%s/%s/%s?%s', 206 | rawurlencode($workspaceSlug), 207 | rawurlencode($templateSlug), 208 | rawurlencode($inputField), 209 | $queryString 210 | ); 211 | 212 | // Generate signature 213 | $signature = hash_hmac('sha256', $stringToSign, $this->secret); 214 | 215 | // Add signature to query string 216 | $finalQueryString = $queryString . '&sig=' . rawurlencode('sha256:' . $signature); 217 | 218 | // Build final URL 219 | return sprintf( 220 | 'https://%s.tlcdn.com/%s/%s?%s', 221 | rawurlencode($workspaceSlug), 222 | rawurlencode($templateSlug), 223 | rawurlencode($inputField), 224 | $finalQueryString 225 | ); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /test/simple/TransloaditRequestTest.php: -------------------------------------------------------------------------------- 1 | request = new TransloaditRequest(); 13 | } 14 | 15 | public function testConstructor() { 16 | $this->assertInstanceOf('transloadit\\CurlRequest', $this->request); 17 | } 18 | 19 | public function testAttributes() { 20 | $this->assertEquals($this->request->endpoint, 'https://api2.transloadit.com'); 21 | $this->assertEquals($this->request->path, null); 22 | $this->assertEquals($this->request->key, null); 23 | $this->assertEquals($this->request->secret, null); 24 | $this->assertEquals($this->request->params, []); 25 | $this->assertEquals($this->request->expires, '+2 hours'); 26 | $this->assertEquals('Expect:', $this->request->headers[0]); 27 | $this->assertContains('Transloadit-Client: php-sdk:%s', $this->request->headers); 28 | } 29 | 30 | public function testInit() { 31 | $METHOD = 'CONNECT'; 32 | $PATH = '/foo'; 33 | 34 | $this->request->setMethodAndPath($METHOD, $PATH); 35 | $this->assertEquals($METHOD, $this->request->method); 36 | $this->assertEquals($PATH, $this->request->path); 37 | } 38 | 39 | public function testPrepare() { 40 | $this->request = $this->getMockBuilder(TransloaditRequest::class) 41 | ->setMethods(['getParamsString', 'signString', 'configureUrl']) 42 | ->getMock(); 43 | 44 | $PARAMS_STRING = '{super}'; 45 | $SIGNATURE_STRING = 'dsasjhdsajda'; 46 | 47 | $this->request 48 | ->method('getParamsString') 49 | ->willReturn($PARAMS_STRING); 50 | 51 | $this->request 52 | ->method('signString') 53 | ->with($this->equalTo($PARAMS_STRING)) 54 | ->willReturn($SIGNATURE_STRING); 55 | 56 | $this->request 57 | ->method('configureUrl'); 58 | 59 | $this->request->prepare(); 60 | $this->assertEquals($PARAMS_STRING, $this->request->fields['params']); 61 | $this->assertEquals($SIGNATURE_STRING, $this->request->fields['signature']); 62 | 63 | // Without signature 64 | $this->request = $this->getMockBuilder(TransloaditRequest::class) 65 | ->setMethods(['getParamsString', 'signString', 'configureUrl']) 66 | ->getMock(); 67 | $SIGNATURE_STRING = null; 68 | 69 | $this->request 70 | ->method('getParamsString') 71 | ->will($this->returnValue($PARAMS_STRING)); 72 | 73 | $this->request 74 | ->method('signString') 75 | ->with($this->equalTo($PARAMS_STRING)) 76 | ->will($this->returnValue($SIGNATURE_STRING)); 77 | 78 | $this->request 79 | ->method('configureUrl'); 80 | 81 | $this->request->prepare(); 82 | $this->assertEquals($PARAMS_STRING, $this->request->fields['params']); 83 | $this->assertArrayNotHasKey('signature', $this->request->fields); 84 | } 85 | 86 | public function testConfigureUrl() { 87 | $PATH = $this->request->path = '/foo'; 88 | $ENDPOINT = $this->request->endpoint = 'ftp://bar.com'; 89 | $this->request->configureUrl(); 90 | 91 | $this->assertEquals('ftp://bar.com/foo', $this->request->url); 92 | 93 | $URL = $this->request->url = 'http://custom.org/manual'; 94 | $this->request->configureUrl(); 95 | $this->assertEquals($URL, $this->request->url); 96 | } 97 | 98 | public function testSignString() { 99 | // No secret, no signature 100 | $this->assertEquals(null, $this->request->signString('foo')); 101 | 102 | // Verify the test vector given in the documentation, see: http://transloadit.com/docs/authentication 103 | $this->request->secret = 'd805593620e689465d7da6b8caf2ac7384fdb7e9'; 104 | $expectedSignature = 'fec703ccbe36b942c90d17f64b71268ed4f5f512'; 105 | 106 | $params = '{"auth":{"expires":"2010\/10\/19 09:01:20+00:00","key":"2b0c45611f6440dfb64611e872ec3211"},"steps":{"encode":{"robot":"\/video\/encode"}}}'; 107 | $signature = $this->request->signString($params); 108 | $this->assertEquals($expectedSignature, $signature); 109 | } 110 | 111 | public function testGetParamsString() { 112 | $this->request->key = 'dskjadjk2j42jkh4'; 113 | $PARAMS = $this->request->params = ['foo' => 'bar']; 114 | $paramsString = $this->request->getParamsString(); 115 | $params = json_decode($paramsString, true); 116 | 117 | $this->assertEquals($this->request->key, $params['auth']['key']); 118 | $this->assertEquals(gmdate('Y/m/d H:i:s+00:00', strtotime($this->request->expires)), $params['auth']['expires']); 119 | $this->assertEquals($PARAMS['foo'], $params['foo']); 120 | } 121 | 122 | public function testSignatureParityWithNodeCli(): void { 123 | if (getenv('TEST_NODE_PARITY') !== '1') { 124 | $this->markTestSkipped('Parity testing not enabled'); 125 | } 126 | 127 | $request = new TransloaditRequest(); 128 | $request->key = 'cli-key'; 129 | $request->secret = 'cli-secret'; 130 | $request->expires = '2025-01-02 00:00:00+00:00'; 131 | $request->params = [ 132 | 'auth' => ['expires' => '2025-01-02 00:00:00+00:00'], 133 | 'steps' => [ 134 | 'resize' => [ 135 | 'robot' => '/image/resize', 136 | 'width' => 320, 137 | ], 138 | ], 139 | ]; 140 | 141 | $cliResult = $this->getCliSignature([ 142 | 'auth' => ['expires' => '2025-01-02 00:00:00+00:00'], 143 | 'steps' => [ 144 | 'resize' => [ 145 | 'robot' => '/image/resize', 146 | 'width' => 320, 147 | ], 148 | ], 149 | ], 'cli-key', 'cli-secret', 'sha1'); 150 | 151 | $this->assertNotNull($cliResult); 152 | $this->assertArrayHasKey('signature', $cliResult); 153 | $this->assertArrayHasKey('params', $cliResult); 154 | 155 | $cliParams = json_decode($cliResult['params'], true); 156 | $phpParams = json_decode($request->getParamsString(), true); 157 | 158 | $this->assertEquals('cli-key', $cliParams['auth']['key']); 159 | $this->assertEquals($phpParams['auth']['expires'], $cliParams['auth']['expires']); 160 | $this->assertEquals( 161 | $phpParams['steps']['resize']['robot'], 162 | $cliParams['steps']['resize']['robot'] 163 | ); 164 | $this->assertEquals( 165 | $phpParams['steps']['resize']['width'], 166 | $cliParams['steps']['resize']['width'] 167 | ); 168 | 169 | $expectedSignature = hash_hmac('sha1', $cliResult['params'], 'cli-secret'); 170 | $this->assertEquals('sha1:' . $expectedSignature, $cliResult['signature']); 171 | } 172 | 173 | public function testExecute() { 174 | // Can't test this method because PHP doesn't allow stubbing the calls 175 | // to curl easily. However, the method hardly contains any logic as all 176 | // of that is located in other methods. 177 | $this->assertTrue(true); 178 | } 179 | 180 | private function getCliSignature(array $params, string $key, string $secret, ?string $algorithm = null): ?array { 181 | if (getenv('TEST_NODE_PARITY') !== '1') { 182 | return null; 183 | } 184 | 185 | exec('command -v npm 2>/dev/null', $output, $returnVar); 186 | if ($returnVar !== 0) { 187 | throw new \RuntimeException('npm command not found. Please install Node.js (which includes npm).'); 188 | } 189 | 190 | try { 191 | $jsonInput = json_encode($params, JSON_THROW_ON_ERROR); 192 | } catch (\JsonException $e) { 193 | throw new \RuntimeException('Failed to encode parameters for Node parity test: ' . $e->getMessage(), 0, $e); 194 | } 195 | 196 | $command = 'npm exec --yes --package transloadit@4.0.5 -- transloadit sig'; 197 | if ($algorithm !== null) { 198 | $command .= ' --algorithm ' . escapeshellarg($algorithm); 199 | } 200 | 201 | $descriptorspec = [ 202 | 0 => ["pipe", "r"], // stdin 203 | 1 => ["pipe", "w"], // stdout 204 | 2 => ["pipe", "w"], // stderr 205 | ]; 206 | 207 | $originalKey = getenv('TRANSLOADIT_KEY'); 208 | $originalSecret = getenv('TRANSLOADIT_SECRET'); 209 | $originalAuthKey = getenv('TRANSLOADIT_AUTH_KEY'); 210 | $originalAuthSecret = getenv('TRANSLOADIT_AUTH_SECRET'); 211 | 212 | putenv('TRANSLOADIT_KEY=' . $key); 213 | putenv('TRANSLOADIT_SECRET=' . $secret); 214 | putenv('TRANSLOADIT_AUTH_KEY=' . $key); 215 | putenv('TRANSLOADIT_AUTH_SECRET=' . $secret); 216 | 217 | try { 218 | $process = proc_open($command, $descriptorspec, $pipes); 219 | 220 | if (!is_resource($process)) { 221 | throw new \RuntimeException('Failed to start transloadit CLI sig command'); 222 | } 223 | 224 | fwrite($pipes[0], $jsonInput); 225 | fclose($pipes[0]); 226 | 227 | $stdout = stream_get_contents($pipes[1]); 228 | $stderr = stream_get_contents($pipes[2]); 229 | 230 | fclose($pipes[1]); 231 | fclose($pipes[2]); 232 | 233 | $exitCode = proc_close($process); 234 | 235 | if ($exitCode !== 0) { 236 | $message = trim($stderr) !== '' ? trim($stderr) : 'Command exited with status ' . $exitCode; 237 | throw new \RuntimeException('transloadit CLI sig command failed: ' . $message); 238 | } 239 | 240 | return json_decode(trim($stdout), true, 512, JSON_THROW_ON_ERROR); 241 | } finally { 242 | if ($originalKey !== false) { 243 | putenv('TRANSLOADIT_KEY=' . $originalKey); 244 | } else { 245 | putenv('TRANSLOADIT_KEY'); 246 | } 247 | 248 | if ($originalSecret !== false) { 249 | putenv('TRANSLOADIT_SECRET=' . $originalSecret); 250 | } else { 251 | putenv('TRANSLOADIT_SECRET'); 252 | } 253 | 254 | if ($originalAuthKey !== false) { 255 | putenv('TRANSLOADIT_AUTH_KEY=' . $originalAuthKey); 256 | } else { 257 | putenv('TRANSLOADIT_AUTH_KEY'); 258 | } 259 | 260 | if ($originalAuthSecret !== false) { 261 | putenv('TRANSLOADIT_AUTH_SECRET=' . $originalAuthSecret); 262 | } else { 263 | putenv('TRANSLOADIT_AUTH_SECRET'); 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /test/simple/TransloaditTest.php: -------------------------------------------------------------------------------- 1 | transloadit = new Transloadit(); 14 | } 15 | 16 | public function testConstructor() { 17 | $transloadit = new Transloadit(['endpoint' => 'foobar']); 18 | $this->assertEquals('foobar', $transloadit->endpoint); 19 | } 20 | 21 | public function testAttributes() { 22 | $this->assertEquals($this->transloadit->key, null); 23 | $this->assertEquals($this->transloadit->secret, null); 24 | } 25 | 26 | public function testCreateAssembly() { 27 | $transloadit = $this->getMockBuilder(Transloadit::class) 28 | ->setMethods(['request']) 29 | ->getMock(); 30 | $assembly = $this->getMockBuilder(TransloaditResponse::class) 31 | ->getMock(); 32 | 33 | $options = ['foo' => 'bar']; 34 | 35 | $transloadit 36 | ->method('request') 37 | ->with($this->equalTo($options + [ 38 | 'method' => 'POST', 39 | 'path' => '/assemblies', 40 | ])) 41 | ->willReturn($assembly); 42 | 43 | $this->assertEquals($assembly, $transloadit->createAssembly($options)); 44 | } 45 | 46 | public function testCancelAssembly() { 47 | $transloadit = $this->getMockBuilder(Transloadit::class) 48 | ->setMethods(['request']) 49 | ->getMock(); 50 | $assembly = $this->getMockBuilder(TransloaditResponse::class) 51 | ->getMock(); 52 | $response = $this->getMockBuilder(TransloaditResponse::class) 53 | ->getMock(); 54 | 55 | $assemblyId = 'b7716f21ba1a400f8b1a60a6e1c6acf1'; 56 | $assembly->data = ['assembly_url' => sprintf('https://api2-phpsdktest.transloadit.com/assemblies/%s', $assemblyId)]; 57 | 58 | $transloadit 59 | ->method('request') 60 | ->withConsecutive( 61 | [$this->equalTo([ 62 | 'method' => 'GET', 63 | 'path' => sprintf('/assemblies/%s', $assemblyId), 64 | ]) 65 | ], 66 | [$this->equalTo([ 67 | 'method' => 'DELETE', 68 | 'path' => sprintf('/assemblies/%s', $assemblyId), 69 | 'host' => 'api2-phpsdktest.transloadit.com', 70 | ]) 71 | ], 72 | ) 73 | ->willReturnOnConsecutiveCalls($assembly, $response); 74 | 75 | $this->assertEquals($response, $transloadit->cancelAssembly($assemblyId)); 76 | } 77 | 78 | public function testRequest() { 79 | $this->transloadit->key = 'my-key'; 80 | $this->transloadit->secret = 'my-secret'; 81 | $request = $this->transloadit->request(['url' => 'foobar'], false); 82 | 83 | $this->assertEquals($this->transloadit->key, $request->key); 84 | $this->assertEquals($this->transloadit->secret, $request->secret); 85 | $this->assertEquals('foobar', $request->url); 86 | 87 | // Unfortunately we can't test the $execute parameter because PHP 88 | // is a little annoying. But that's ok for now. 89 | } 90 | 91 | public function testResponse() { 92 | $response = Transloadit::response(); 93 | $this->assertEquals(false, $response); 94 | 95 | $data = ['foo' => 'bar']; 96 | $_POST['transloadit'] = json_encode($data); 97 | $response = Transloadit::response(); 98 | $this->assertInstanceOf(TransloaditResponse::class, $response); 99 | $this->assertEquals($data, $response->data); 100 | 101 | 102 | // Can't really test the $_GET['assembly_url'] case because of PHP for now. 103 | } 104 | 105 | public function testCreateAssemblyForm() { 106 | $transloadit = $this->getMockBuilder(Transloadit::class) 107 | ->setMethods(['request']) 108 | ->getMock(); 109 | $assembly = $this->getMockBuilder(TransloaditResponse::class) 110 | ->setMethods(['prepare']) 111 | ->getMock(); 112 | 113 | $assembly->method = 'ROCK'; 114 | $assembly->url = 'http://api999.transloadit.com/assemblies'; 115 | $assembly->fields = [ 116 | 'foo' => 'bar"bar', 117 | 'hey' => 'you', 118 | ]; 119 | $options = ['foo' => 'bar']; 120 | 121 | $transloadit 122 | ->method('request') 123 | ->with($this->equalTo($options + [ 124 | 'method' => 'POST', 125 | 'path' => '/assemblies', 126 | ]), $this->equalTo(false)) 127 | ->willReturn($assembly); 128 | 129 | $assembly 130 | ->method('prepare'); 131 | 132 | $options['attributes'] = ['class' => 'nice']; 133 | $tags = explode("\n", $transloadit->createAssemblyForm($options)); 134 | 135 | $formTag = array_shift($tags); 136 | $this->assertTrue(preg_match('/action="http:\/\/api999\.transloadit\.com\/assemblies"/', $formTag) !== false); 137 | $this->assertTrue(preg_match('/method="ROCK"/', $formTag) !== false); 138 | $this->assertTrue(preg_match('/enctype="multipart\/form-data"/', $formTag) !== false); 139 | $this->assertTrue(preg_match('/class="nice"/', $formTag) !== false); 140 | 141 | foreach ($assembly->fields as $field => $val) { 142 | $inputTag = array_shift($tags); 143 | $this->assertTrue(preg_match('/type="hidden"/', $inputTag) !== false); 144 | $this->assertTrue(preg_match('/name="' . $field . '"/', $inputTag) !== false); 145 | $this->assertTrue(preg_match('/value="' . $val . '"/', $inputTag) !== false); 146 | } 147 | } 148 | 149 | private function getExpectedUrl(array $params): ?string { 150 | if (getenv('TEST_NODE_PARITY') !== '1') { 151 | return null; 152 | } 153 | 154 | exec('command -v npm 2>/dev/null', $output, $returnVar); 155 | if ($returnVar !== 0) { 156 | throw new \RuntimeException('npm command not found. Please install Node.js (which includes npm).'); 157 | } 158 | 159 | if (!isset($params['auth_key']) || !isset($params['auth_secret'])) { 160 | throw new \InvalidArgumentException('auth_key and auth_secret are required for parity testing'); 161 | } 162 | 163 | try { 164 | $cliParams = [ 165 | 'workspace' => $params['workspace'], 166 | 'template' => $params['template'], 167 | 'input' => $params['input'], 168 | ]; 169 | if (array_key_exists('url_params', $params)) { 170 | $cliParams['url_params'] = $params['url_params']; 171 | } 172 | if (array_key_exists('expire_at_ms', $params)) { 173 | $cliParams['expire_at_ms'] = $params['expire_at_ms']; 174 | } 175 | $jsonInput = json_encode($cliParams, JSON_THROW_ON_ERROR); 176 | } catch (\JsonException $e) { 177 | throw new \RuntimeException('Failed to encode parameters for Node parity test: ' . $e->getMessage(), 0, $e); 178 | } 179 | 180 | $command = 'npm exec --yes --package transloadit@4.0.5 -- transloadit smart_sig'; 181 | 182 | $descriptorspec = [ 183 | 0 => ["pipe", "r"], // stdin 184 | 1 => ["pipe", "w"], // stdout 185 | 2 => ["pipe", "w"], // stderr 186 | ]; 187 | 188 | $originalKey = getenv('TRANSLOADIT_KEY'); 189 | $originalSecret = getenv('TRANSLOADIT_SECRET'); 190 | $originalAuthKey = getenv('TRANSLOADIT_AUTH_KEY'); 191 | $originalAuthSecret = getenv('TRANSLOADIT_AUTH_SECRET'); 192 | 193 | putenv('TRANSLOADIT_KEY=' . $params['auth_key']); 194 | putenv('TRANSLOADIT_SECRET=' . $params['auth_secret']); 195 | putenv('TRANSLOADIT_AUTH_KEY=' . $params['auth_key']); 196 | putenv('TRANSLOADIT_AUTH_SECRET=' . $params['auth_secret']); 197 | 198 | try { 199 | $process = proc_open($command, $descriptorspec, $pipes); 200 | 201 | if (!is_resource($process)) { 202 | throw new \RuntimeException('Failed to start transloadit CLI parity command'); 203 | } 204 | 205 | fwrite($pipes[0], $jsonInput); 206 | fclose($pipes[0]); 207 | 208 | $stdout = stream_get_contents($pipes[1]); 209 | $stderr = stream_get_contents($pipes[2]); 210 | 211 | fclose($pipes[1]); 212 | fclose($pipes[2]); 213 | 214 | $exitCode = proc_close($process); 215 | 216 | if ($exitCode !== 0) { 217 | $message = trim($stderr) !== '' ? trim($stderr) : 'Command exited with status ' . $exitCode; 218 | throw new \RuntimeException('transloadit CLI parity command failed: ' . $message); 219 | } 220 | 221 | return trim($stdout); 222 | } finally { 223 | if ($originalKey !== false) { 224 | putenv('TRANSLOADIT_KEY=' . $originalKey); 225 | } else { 226 | putenv('TRANSLOADIT_KEY'); 227 | } 228 | 229 | if ($originalSecret !== false) { 230 | putenv('TRANSLOADIT_SECRET=' . $originalSecret); 231 | } else { 232 | putenv('TRANSLOADIT_SECRET'); 233 | } 234 | 235 | if ($originalAuthKey !== false) { 236 | putenv('TRANSLOADIT_AUTH_KEY=' . $originalAuthKey); 237 | } else { 238 | putenv('TRANSLOADIT_AUTH_KEY'); 239 | } 240 | 241 | if ($originalAuthSecret !== false) { 242 | putenv('TRANSLOADIT_AUTH_SECRET=' . $originalAuthSecret); 243 | } else { 244 | putenv('TRANSLOADIT_AUTH_SECRET'); 245 | } 246 | } 247 | } 248 | 249 | private function assertParityWithNode(string $url, array $params, string $message = ''): void { 250 | $expectedUrl = $this->getExpectedUrl($params); 251 | if ($expectedUrl !== null) { 252 | $this->assertEquals($expectedUrl, $url, $message ?: 'URL should match Node.js reference implementation'); 253 | } 254 | } 255 | 256 | private function debugUrls(string $phpUrl, string $nodeUrl, string $message = ''): void { 257 | echo "\n\nDebug $message:\n"; 258 | echo "PHP URL: $phpUrl\n"; 259 | echo "Node URL: $nodeUrl\n\n"; 260 | } 261 | 262 | public function testSignedSmartCDNUrl() { 263 | $transloadit = new Transloadit([ 264 | 'key' => 'test-key', 265 | 'secret' => 'test-secret' 266 | ]); 267 | 268 | // Use fixed timestamp for all tests 269 | $expireAtMs = 1732550672867; 270 | 271 | // Test basic URL generation 272 | $params = [ 273 | 'workspace' => 'workspace', 274 | 'template' => 'template', 275 | 'input' => 'file.jpg', 276 | 'auth_key' => 'test-key', 277 | 'auth_secret' => 'test-secret', 278 | 'expire_at_ms' => $expireAtMs 279 | ]; 280 | $url = $transloadit->signedSmartCDNUrl( 281 | $params['workspace'], 282 | $params['template'], 283 | $params['input'], 284 | [], 285 | $expireAtMs 286 | ); 287 | $nodeUrl = $this->getExpectedUrl($params); 288 | $expectedUrl = 'https://workspace.tlcdn.com/template/file.jpg?auth_key=test-key&exp=1732550672867&sig=sha256%3Ad994b8a737db1c43d6e04a07018dc33e8e28b23b27854bd6383d828a212cfffb'; 289 | $this->assertEquals($expectedUrl, $url, 'PHP URL should match expected'); 290 | $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); 291 | 292 | // Test with input field 293 | $params['input'] = 'input.jpg'; 294 | $url = $transloadit->signedSmartCDNUrl( 295 | $params['workspace'], 296 | $params['template'], 297 | $params['input'], 298 | [], 299 | $expireAtMs 300 | ); 301 | $nodeUrl = $this->getExpectedUrl($params); 302 | $expectedUrl = 'https://workspace.tlcdn.com/template/input.jpg?auth_key=test-key&exp=1732550672867&sig=sha256%3A75991f02828d194792c9c99f8fea65761bcc4c62dbb287a84f642033128297c0'; 303 | $this->assertEquals($expectedUrl, $url, 'PHP URL should match expected'); 304 | $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); 305 | 306 | // Test with additional params 307 | $params['input'] = 'file.jpg'; 308 | $params['url_params'] = ['width' => 100]; 309 | $url = $transloadit->signedSmartCDNUrl( 310 | $params['workspace'], 311 | $params['template'], 312 | $params['input'], 313 | $params['url_params'], 314 | $expireAtMs 315 | ); 316 | $nodeUrl = $this->getExpectedUrl($params); 317 | $expectedUrl = 'https://workspace.tlcdn.com/template/file.jpg?auth_key=test-key&exp=1732550672867&width=100&sig=sha256%3Ae5271d8fb6482d9351ebe4285b6fc75539c4d311ff125c4d76d690ad71c258ef'; 318 | $this->assertEquals($expectedUrl, $url, 'PHP URL should match expected'); 319 | $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); 320 | 321 | // Test with empty param string 322 | $params['url_params'] = ['width' => '', 'height' => '200']; 323 | $url = $transloadit->signedSmartCDNUrl( 324 | $params['workspace'], 325 | $params['template'], 326 | $params['input'], 327 | $params['url_params'], 328 | $expireAtMs 329 | ); 330 | $nodeUrl = $this->getExpectedUrl($params); 331 | $expectedUrl = 'https://workspace.tlcdn.com/template/file.jpg?auth_key=test-key&exp=1732550672867&height=200&width=&sig=sha256%3A1a26733c859f070bc3d83eb3174650d7a0155642e44a5ac448a43bc728bc0f85'; 332 | $this->assertEquals($expectedUrl, $url, 'PHP URL should match expected'); 333 | $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); 334 | 335 | // Test with null width parameter (should be excluded) 336 | $params['url_params'] = ['width' => null, 'height' => '200']; 337 | $url = $transloadit->signedSmartCDNUrl( 338 | $params['workspace'], 339 | $params['template'], 340 | $params['input'], 341 | $params['url_params'], 342 | $expireAtMs 343 | ); 344 | $nodeUrl = $this->getExpectedUrl($params); 345 | $expectedUrl = 'https://workspace.tlcdn.com/template/file.jpg?auth_key=test-key&exp=1732550672867&height=200&sig=sha256%3Adb740ebdfad6e766ebf6516ed5ff6543174709f8916a254f8d069c1701cef517'; 346 | $this->assertEquals($expectedUrl, $url, 'PHP URL should match expected'); 347 | $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); 348 | 349 | // Test with only empty width parameter 350 | $params['url_params'] = ['width' => '']; 351 | $url = $transloadit->signedSmartCDNUrl( 352 | $params['workspace'], 353 | $params['template'], 354 | $params['input'], 355 | $params['url_params'], 356 | $expireAtMs 357 | ); 358 | $nodeUrl = $this->getExpectedUrl($params); 359 | $expectedUrl = 'https://workspace.tlcdn.com/template/file.jpg?auth_key=test-key&exp=1732550672867&width=&sig=sha256%3A840426f9ac72dde02fd080f09b2304d659fdd41e630b1036927ec1336c312e9d'; 360 | $this->assertEquals($expectedUrl, $url, 'PHP URL should match expected'); 361 | $this->assertEquals($expectedUrl, $nodeUrl, 'Node.js URL should match expected'); 362 | } 363 | 364 | public function testTransloaditCliRequiredForParityTesting(): void { 365 | if (getenv('TEST_NODE_PARITY') !== '1') { 366 | $this->markTestSkipped('Parity testing not enabled'); 367 | } 368 | 369 | // Temporarily override PATH to simulate missing npm 370 | $originalPath = getenv('PATH'); 371 | putenv('PATH='); 372 | 373 | try { 374 | $params = [ 375 | 'workspace' => 'test', 376 | 'template' => 'test', 377 | 'input' => 'test.jpg', 378 | 'auth_key' => 'test', 379 | 'auth_secret' => 'test' 380 | ]; 381 | $this->getExpectedUrl($params); 382 | $this->fail('Expected RuntimeException when npm is not available'); 383 | } catch (\RuntimeException $e) { 384 | $this->assertStringContainsString('npm command not found', $e->getMessage()); 385 | } finally { 386 | // Restore original PATH 387 | putenv("PATH=$originalPath"); 388 | } 389 | } 390 | 391 | public function testExpireAtMs(): void { 392 | $transloadit = new Transloadit([ 393 | 'key' => 'test-key', 394 | 'secret' => 'test-secret' 395 | ]); 396 | 397 | // Test with explicit expireAtMs 398 | $expireAtMs = 1732550672867; 399 | $params = [ 400 | 'workspace' => 'workspace', 401 | 'template' => 'template', 402 | 'input' => 'file.jpg', 403 | 'auth_key' => 'test-key', 404 | 'auth_secret' => 'test-secret', 405 | 'expire_at_ms' => $expireAtMs 406 | ]; 407 | 408 | $url = $transloadit->signedSmartCDNUrl( 409 | $params['workspace'], 410 | $params['template'], 411 | $params['input'], 412 | [], 413 | $params['expire_at_ms'] 414 | ); 415 | 416 | $this->assertStringContainsString("exp=$expireAtMs", $url); 417 | $this->assertParityWithNode($url, $params); 418 | 419 | // Test default expiry (should be about 1 hour from now) 420 | unset($params['expire_at_ms']); 421 | $url = $transloadit->signedSmartCDNUrl( 422 | $params['workspace'], 423 | $params['template'], 424 | $params['input'] 425 | ); 426 | 427 | $matches = []; 428 | preg_match('/exp=(\d+)/', $url, $matches); 429 | $this->assertNotEmpty($matches[1], 'URL should contain expiry timestamp'); 430 | 431 | $expiry = (int)$matches[1]; 432 | $now = time() * 1000; 433 | $oneHour = 60 * 60 * 1000; 434 | 435 | $this->assertGreaterThan($now, $expiry, 'Expiry should be in the future'); 436 | $this->assertLessThan($now + $oneHour + 5000, $expiry, 'Expiry should be about 1 hour from now'); 437 | $this->assertGreaterThan($now + $oneHour - 5000, $expiry, 'Expiry should be about 1 hour from now'); 438 | 439 | // For parity test, set the exact expiry time to match Node.js 440 | $params['expire_at_ms'] = $expiry; 441 | $this->assertParityWithNode($url, $params); 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transloadit PHP SDK 2 | 3 | [![Test Actions Status][test_badge]][test_link] 4 | [![Code coverage][codecov_badge]][codecov_link] 5 | ![Packagist PHP Version Support][php_verison_badge] 6 | [![License][licence_badge]][licence_link] 7 | 8 | ## Introduction 9 | 10 | The Transloadit PHP SDK provides a simple and efficient way to interact with Transloadit's file processing service in your PHP applications. With this SDK, you can easily: 11 | 12 | - Create and manage file upload assemblies 13 | - Use pre-defined templates for common file processing tasks 14 | - Handle notifications and retrieve assembly statuses 15 | - Integrate Transloadit's powerful file processing capabilities into your PHP projects 16 | 17 | This SDK simplifies the process of working with Transloadit's REST API, allowing you to focus on building great applications without worrying about the complexities of file processing. 18 | 19 | ## Install 20 | 21 | ``` 22 | composer require transloadit/php-sdk 23 | ``` 24 | 25 | Keep your Transloadit account's Auth Key & Secret nearby. You can check 26 | the [API credentials](https://transloadit.com/accounts/credentials) page for 27 | these values. 28 | 29 | ## Usage 30 | 31 | 32 | 33 | ### 1. Upload and resize an image from your server 34 | 35 | This example demonstrates how you can use the SDK to create an Assembly 36 | on your server. 37 | 38 | It takes a sample image file, uploads it to Transloadit, and starts a 39 | resizing job on it. 40 | 41 | ```php 42 | 'MY_TRANSLOADIT_KEY', 49 | 'secret' => 'MY_TRANSLOADIT_SECRET', 50 | ]); 51 | 52 | $response = $transloadit->createAssembly([ 53 | 'files' => ['/PATH/TO/FILE.jpg'], 54 | 'params' => [ 55 | 'steps' => [ 56 | 'resize' => [ 57 | 'robot' => '/image/resize', 58 | 'width' => 200, 59 | 'height' => 100, 60 | ], 61 | ], 62 | ], 63 | ]); 64 | 65 | // Show the results of the assembly we spawned 66 | echo '
';
 67 | print_r($response);
 68 | echo '
'; 69 | 70 | ``` 71 | 72 | ### 2. Create a simple end-user upload form 73 | 74 | This example shows you how to create a simple Transloadit upload form 75 | that redirects back to your site after the upload is done. 76 | 77 | Once the script receives the redirect request, the current status for 78 | this Assembly is shown using `Transloadit::response()`. 79 | 80 |
81 | Note: There is no guarantee that the Assembly has already finished 82 | executing by the time the $response is fetched. You should use 83 | the notify_url parameter for this. 84 |
85 | 86 | ```php 87 | 'MY_TRANSLOADIT_KEY', 94 | 'secret' => 'MY_TRANSLOADIT_SECRET', 95 | ]); 96 | 97 | // Check if this request is a Transloadit redirect_url notification. 98 | // If so fetch the response and output the current assembly status: 99 | $response = Transloadit::response(); 100 | if ($response) { 101 | echo '

Assembly Status:

'; 102 | echo '
';
103 |   print_r($response);
104 |   echo '
'; 105 | exit; 106 | } 107 | 108 | // This should work on most environments, but you might have to modify 109 | // this for your particular setup. 110 | $redirectUrl = sprintf('http://%s%s', $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']); 111 | 112 | // Setup a simple file upload form that resizes an image to 200x100px 113 | echo $transloadit->createAssemblyForm([ 114 | 'params' => [ 115 | 'steps' => [ 116 | 'resize' => [ 117 | 'robot' => '/image/resize', 118 | 'width' => 200, 119 | 'height' => 100, 120 | ], 121 | ], 122 | 'redirect_url' => $redirectUrl, 123 | ], 124 | ]); 125 | ?> 126 |

Pick an image to resize

127 | 128 | 129 |
130 | 131 | ``` 132 | 133 | ### 3. Use Uppy for file uploads 134 | 135 | We recommend using [Uppy](https://transloadit.com/docs/sdks/uppy/), our next-gen file uploader for the web, instead of the jQuery SDK. Uppy provides a more modern, flexible, and feature-rich solution for handling file uploads with Transloadit. 136 | 137 | To integrate Uppy with your PHP backend: 138 | 139 | 1. Include Uppy in your HTML: 140 | 141 | ```html 142 | 146 | 147 | ``` 148 | 149 | 2. Initialize Uppy with Transloadit plugin: 150 | 151 | ```html 152 |
153 | 154 | 172 | ``` 173 | 174 | 3. Handle the assembly status on your PHP backend: 175 | 176 | Create a new file named `transloadit_notify.php` in your project: 177 | 178 | ```php 179 | 'MY_TRANSLOADIT_KEY', 186 | 'secret' => 'MY_TRANSLOADIT_SECRET', 187 | ]); 188 | 189 | $response = Transloadit::response(); 190 | if ($response) { 191 | // Process the assembly result 192 | $assemblyId = $response->data['assembly_id']; 193 | $assemblyStatus = $response->data['ok']; 194 | 195 | // You can store the assembly information in your database 196 | // or perform any other necessary actions here 197 | 198 | // Log the response for debugging 199 | error_log('Transloadit Assembly Completed: ' . $assemblyId); 200 | error_log('Assembly Status: ' . ($assemblyStatus ? 'Success' : 'Failed')); 201 | 202 | // Optionally, you can write the response to a file 203 | file_put_contents('transloadit_response_' . $assemblyId . '.json', json_encode($response->data)); 204 | 205 | // Send a 200 OK response to Transloadit 206 | http_response_code(200); 207 | echo 'OK'; 208 | } else { 209 | // If it's not a Transloadit notification, return a 400 Bad Request 210 | http_response_code(400); 211 | echo 'Bad Request'; 212 | } 213 | ?> 214 | ``` 215 | 216 | Make sure to replace `'https://your-site.com/transloadit_notify.php'` with the actual URL where you'll host this PHP script. 217 | 218 | For more detailed information on using Uppy with Transloadit, please refer to our [Uppy documentation](https://transloadit.com/docs/sdks/uppy/). 219 | 220 | ### 4. Fetch the Assembly Status JSON 221 | 222 | You can use the `getAssembly` method to get the Assembly Status. 223 | 224 | ```php 225 | 'MY_TRANSLOADIT_KEY', 231 | 'secret' => 'MY_TRANSLOADIT_SECRET', 232 | ]); 233 | 234 | $response = $transloadit->getAssembly($assemblyId); 235 | 236 | echo '
';
237 | print_r($response);
238 | echo '
'; 239 | 240 | ``` 241 | 242 | ### 5. Create an Assembly with a Template. 243 | 244 | This example demonstrates how you can use the SDK to create an Assembly 245 | with Templates. 246 | 247 | You are expected to create a Template on your Transloadit account dashboard 248 | and add the Template ID here. 249 | 250 | ```php 251 | 'MY_TRANSLOADIT_KEY', 258 | 'secret' => 'MY_TRANSLOADIT_SECRET', 259 | ]); 260 | 261 | $response = $transloadit->createAssembly([ 262 | 'files' => ['/PATH/TO/FILE.jpg'], 263 | 'params' => [ 264 | 'template_id' => 'MY_TEMPLATE_ID', 265 | ], 266 | ]); 267 | 268 | // Show the results of the assembly we spawned 269 | echo '
';
270 | print_r($response);
271 | echo '
'; 272 | 273 | ``` 274 | 275 | ### Signature Auth (Assemblies) 276 | 277 | Signature Authentication is done by the PHP SDK by default internally so you do not need to worry about this :) 278 | 279 | If you script the same request payload multiple times in quick succession (for example inside a health check or tight integration test loop), add a random nonce to keep each signature unique: 280 | 281 | ```php 282 | $params = [ 283 | 'auth' => [ 284 | 'key' => 'MY_TRANSLOADIT_KEY', 285 | 'expires' => gmdate('Y/m/d H:i:s+00:00', strtotime('+2 hours')), 286 | 'nonce' => bin2hex(random_bytes(16)), 287 | ], 288 | 'steps' => [ 289 | // … 290 | ], 291 | ]; 292 | ``` 293 | 294 | The nonce is optional for regular usage, but including it in heavily scripted flows prevents Transloadit from rejecting repeated identical signatures. 295 | 296 | ### Signature Auth (Smart CDN) 297 | 298 | You can use the `signedSmartCDNUrl` method to generate signed URLs for Transloadit's [Smart CDN](https://transloadit.com/services/content-delivery/): 299 | 300 | ```php 301 | 'MY_TRANSLOADIT_KEY', 308 | 'secret' => 'MY_TRANSLOADIT_SECRET', 309 | ]); 310 | 311 | // Basic usage 312 | $url = $transloadit->signedSmartCDNUrl( 313 | 'your-workspace-slug', 314 | 'your-template-slug', 315 | 'avatars/jane.jpg' 316 | ); 317 | 318 | // Advanced usage with custom parameters and expiry 319 | $url = $transloadit->signedSmartCDNUrl( 320 | 'your-workspace-slug', 321 | 'your-template-slug', 322 | 'avatars/jane.jpg', 323 | ['width' => 100, 'height' => 100], // Additional parameters 324 | 1732550672867, // Expiry date in milliseconds since epoch 325 | ); 326 | 327 | echo $url; 328 | ``` 329 | 330 | The generated URL will be in the format: 331 | 332 | ``` 333 | https://{workspace-slug}.tlcdn.com/{template-slug}/{input-field}?{query-params}&sig=sha256:{signature} 334 | ``` 335 | 336 | Note that: 337 | 338 | - The URL will expire after the specified time (default: 1 hour) 339 | - All parameters are properly encoded 340 | - The signature is generated using HMAC SHA-256 341 | - Query parameters are sorted alphabetically before signing 342 | 343 | ## Example 344 | 345 | For fully working examples take a look at [`examples/`](https://github.com/transloadit/php-sdk/tree/HEAD/examples). 346 | 347 | ## API 348 | 349 | ### $Transloadit = new Transloadit($properties = []); 350 | 351 | Creates a new Transloadit instance and applies the given $properties. 352 | 353 | #### $Transloadit->key = null; 354 | 355 | The auth key of your Transloadit account. 356 | 357 | #### $Transloadit->secret = null; 358 | 359 | The auth secret of your Transloadit account. 360 | 361 | #### $Transloadit->request($options = [], $execute = true); 362 | 363 | Creates a new `TransloaditRequest` using the `$Transloadit->key` and 364 | `$Transloadit->secret` properties. 365 | 366 | If `$execute` is set to `true`, `$TransloaditRequest->execute()` will be 367 | called and used as the return value. 368 | 369 | Otherwise the new `TransloaditRequest` instance is being returned. 370 | 371 | #### $Transloadit->createAssemblyForm($options = []); 372 | 373 | Creates a new Transloadit assembly form including the hidden 'params' and 374 | 'signature' fields. A closing form tag is not included. 375 | 376 | `$options` is an array of `TransloaditRequest` properties to be used. 377 | For example: `"params"`, `"expires"`, `"endpoint"`, etc.. 378 | 379 | In addition to that, you can also pass an `"attributes"` key, which allows 380 | you to set custom form attributes. For example: 381 | 382 | ```php 383 | $Transloadit->createAssemblyForm(array( 384 | 'attributes' => array( 385 | 'id' => 'my_great_upload_form', 386 | 'class' => 'transloadit_form', 387 | ), 388 | )); 389 | ``` 390 | 391 | #### $Transloadit->createAssembly($options); 392 | 393 | Sends a new assembly request to Transloadit. This is the preferred way of 394 | uploading files from your server. 395 | 396 | `$options` is an array of `TransloaditRequest` properties to be used with the exception that you can 397 | also use the `waitForCompletion` option here: 398 | 399 | `waitForCompletion` is a boolean (default is false) to indicate whether you want to wait for the 400 | Assembly to finish with all encoding results present before the callback is called. If 401 | waitForCompletion is true, this SDK will poll for status updates and return when all encoding work 402 | is done. 403 | 404 | Check example #1 above for more information. 405 | 406 | #### $Transloadit->getAssembly($assemblyId); 407 | 408 | Retrieves the Assembly status json for a given Assembly ID. 409 | 410 | #### $Transloadit->cancelAssembly($assemblyId); 411 | 412 | Cancels an assembly that is currently executing and prevents any further encodings costing money. 413 | 414 | This will result in `ASSEMBLY_NOT_FOUND` errors if invoked on assemblies that are not currently 415 | executing (anymore). 416 | 417 | #### Transloadit::response() 418 | 419 | This static method is used to parse the notifications Transloadit sends to 420 | your server. 421 | 422 | There are two kinds of notifications this method handles: 423 | 424 | - When using the `redirect_url` parameter, and Transloadit redirects 425 | back to your site, a `$_GET['assembly_url']` query parameter gets added. 426 | This method detects the presence of this parameter and fetches the current 427 | assembly status from that url and returns it as a `TransloaditResponse`. 428 | - When using the `notify_url` parameter, Transloadit sends a 429 | `$_POST['transloadit']` parameter. This method detects this, and parses 430 | the notification JSON into a `TransloaditResponse` object for you. 431 | 432 | If the current request does not seem to be invoked by Transloadit, this 433 | method returns `false`. 434 | 435 | ### $TransloaditRequest = new TransloaditRequest($properties = []); 436 | 437 | Creates a new TransloaditRequest instance and applies the given $properties. 438 | 439 | #### $TransloaditRequest->key = null; 440 | 441 | The auth key of your Transloadit account. 442 | 443 | #### $TransloaditRequest->secret = null; 444 | 445 | The auth secret of your Transloadit account. 446 | 447 | #### $TransloaditRequest->method = 'GET'; 448 | 449 | Inherited from `CurlRequest`. Can be used to set the type of request to be 450 | made. 451 | 452 | #### $TransloaditRequest->curlOptions = []; 453 | 454 | Inherited from `CurlRequest`. Can be used to tweak cURL behavior using [any cURL option that your PHP/cURL version supports](https://www.php.net/manual/en/function.curl-setopt.php). 455 | 456 | Here is an [example](examples/6-assembly-with-timeout.php) that illustrates 457 | using this option to change the timeout of a request (drastically, to `1ms`, just to prove you can make the SDK abort after a time of your choosing). 458 | 459 | The default timeouts and options depend on the cURL version on your system and can be verified by checking `phpinfo()` and the [curl_setopt](https://www.php.net/manual/en/function.curl-setopt.php) documentation. 460 | 461 | #### $TransloaditRequest->endpoint = 'https://api2.transloadit.com'; 462 | 463 | The endpoint to send this request to. 464 | 465 | #### $TransloaditRequest->path = null; 466 | 467 | The url path to request. 468 | 469 | #### $TransloaditRequest->url = null; 470 | 471 | Inherited from `CurlRequest`. Lets you overwrite the above endpoint / path 472 | properties with a fully custom url alltogether. 473 | 474 | #### $TransloaditRequest->fields = []; 475 | 476 | A list of additional fields to send along with your request. Transloadit 477 | will include those in all assembly related notifications. 478 | 479 | #### $TransloaditRequest->files = []; 480 | 481 | An array of paths to local files you would like to upload. For example: 482 | 483 | ```php 484 | $TransloaditRequest->files = array('/my/file.jpg'); 485 | ``` 486 | 487 | or 488 | 489 | ```php 490 | $TransloaditRequest->files = array('my_upload' => '/my/file.jpg'); 491 | ``` 492 | 493 | The first example would automatically give your file a field name of 494 | `'file_1'` when executing the request. 495 | 496 | #### $TransloaditRequest->params = []; 497 | 498 | An array representing the JSON params to be send to Transloadit. You 499 | do not have to include an `'auth'` key here, as this class handles that 500 | for you as part of `$TransloaditRequest->prepare()`. 501 | 502 | #### $TransloaditRequest->expires = '+2 hours'; 503 | 504 | If you have configured a '`$TransloaditRequest->secret`', this class will 505 | automatically sign your request. The expires property lets you configure 506 | the duration for which the signature is valid. 507 | 508 | #### $TransloaditRequest->headers = []; 509 | 510 | Lets you send additional headers along with your request. You should not 511 | have to change this property. 512 | 513 | #### $TransloaditRequest->execute() 514 | 515 | Sends this request to Transloadit and returns a `TransloaditResponse` 516 | instance. 517 | 518 | ### $TransloaditResponse = new TransloaditResponse($properties = []); 519 | 520 | Creates a new TransloaditResponse instance and applies the given $properties. 521 | 522 | #### $TransloaditResponse->data = null; 523 | 524 | Inherited from `CurlResponse`. Contains an array of the parsed JSON 525 | response from Transloadit. 526 | 527 | You should generally only access this property after having checked for 528 | errors using `$TransloaditResponse->error()`. 529 | 530 | #### $TransloaditResponse->error(); 531 | 532 | Returns `false` or a string containing an explanation of what went wrong. 533 | 534 | All of the following will cause an error string to be returned: 535 | 536 | - Network issues of any kind 537 | - The Transloadit response JSON contains an `{"error": "..."}` key 538 | - A malformed response was received 539 | 540 | **_Note_**: You will need to set waitForCompletion = True in the $Transloadit->createAssembly($options) function call. 541 | 542 | ## License 543 | 544 | [MIT Licensed](LICENSE) 545 | 546 | [test_badge]: https://github.com/transloadit/php-sdk/actions/workflows/ci.yml/badge.svg 547 | [test_link]: https://github.com/transloadit/php-sdk/actions/workflows/ci.yml 548 | [codecov_badge]: https://codecov.io/gh/transloadit/php-sdk/branch/main/graph/badge.svg 549 | [codecov_link]: https://codecov.io/gh/transloadit/php-sdk 550 | [php_verison_badge]: https://img.shields.io/packagist/php-v/transloadit/php-sdk 551 | [licence_badge]: https://img.shields.io/badge/License-MIT-green.svg 552 | [licence_link]: https://github.com/transloadit/php-sdk/blob/main/LICENSE 553 | --------------------------------------------------------------------------------