├── .gitignore ├── local.settings.sample.json ├── src └── Azserverless │ ├── Context │ └── FunctionContext.php │ └── Runtime │ └── Router.php ├── host.sample.json ├── composer.json ├── LICENSE ├── bin └── serverless.php ├── README.md ├── CODE_OF_CONDUCT.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /local.settings.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "custom", 5 | "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=;AccountKey=;EndpointSuffix=" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Azserverless/Context/FunctionContext.php: -------------------------------------------------------------------------------- 1 | log = $log; 12 | $this->outputs = [ '_none_' => null ]; 13 | } 14 | } -------------------------------------------------------------------------------- /host.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[1.*, 2.0.0)" 6 | }, 7 | "customHandler": { 8 | "description": { 9 | "defaultExecutablePath": "php", 10 | "arguments": [ 11 | "-S", 12 | "0.0.0.0:%FUNCTIONS_CUSTOMHANDLER_PORT%", 13 | "vendor/videlalvaro/azserverless/bin/serverless.php" 14 | ] 15 | }, 16 | "enableForwardingHttpRequest": false 17 | }, 18 | "logging": { 19 | "logLevel": { 20 | "Function.HttpTrigger.User": "Information", 21 | "Function.QueueTrigger.User": "Information", 22 | "default": "Warning" 23 | }, 24 | "applicationInsights": { 25 | "samplingSettings": { 26 | "isEnabled": true 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videlalvaro/azserverless", 3 | "version": "0.0.1-alpha", 4 | "description": "\"PHP Custom Handler for Azure Functions\"", 5 | "keywords": ["azure", "functions", "serverless"], 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Alvaro Videla", 11 | "email": "videlalvaro@gmail.com" 12 | }, 13 | { 14 | "name": "Anthony Chu", 15 | "email": "anthony@anthonychu.ca" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4": { 20 | "Azserverless\\": "src/Azserverless" 21 | } 22 | }, 23 | "require": { 24 | "php": ">=7.2", 25 | "ext-json": "*", 26 | "monolog/monolog": "^2.1" 27 | }, 28 | "bin": [ 29 | "bin/serverless.php" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alvaro Videla 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. -------------------------------------------------------------------------------- /bin/serverless.php: -------------------------------------------------------------------------------- 1 | NULL, 11 | 'ReturnValue' => sprintf("m: %s; f: %s; l: %s\n", $last_error['message'], $last_error['file'], $last_error['line']), 12 | 'Logs' => [] 13 | ]; 14 | header("Content-type: application/json"); 15 | echo(json_encode($response)); 16 | exit(1); 17 | } 18 | } 19 | 20 | if (file_exists(__DIR__ . '/vendor/autoload.php')) { // the script is being run on the project's main folder 21 | require_once __DIR__ . '/vendor/autoload.php'; 22 | } elseif (file_exists(__DIR__ . '/../../../autoload.php')) { // the script is being run out of the vendor/videlalvaro/azserverless/bin folder 23 | /** @noinspection PhpIncludeInspection */ 24 | require_once __DIR__ . '/../../../autoload.php'; 25 | } else { 26 | /** @noinspection PhpIncludeInspection */ 27 | require_once __DIR__ . '/../vendor/autoload.php'; // the script is being run in the project's bin folder 28 | } 29 | 30 | use Azserverless\Runtime\Router; 31 | $router = new Router(getcwd()); 32 | $response = $router->route(); 33 | 34 | header("Content-type: application/json"); 35 | echo(json_encode($response)); 36 | -------------------------------------------------------------------------------- /src/Azserverless/Runtime/Router.php: -------------------------------------------------------------------------------- 1 | baseFunctionsDir = $baseFunctionsDir; 18 | // TODO maybe create a more specific log handler. 19 | // So far TestHandler works for our purposes. 20 | $this->logHandler = new TestHandler(); 21 | $this->log = new Logger('serverless'); 22 | $this->log->pushHandler($this->logHandler); 23 | $this->context = new FunctionContext($this->log); 24 | 25 | set_exception_handler(array($this, 'exceptionHandler')); 26 | } 27 | 28 | protected function exceptionHandler($exception) { 29 | $this->context->log->error($exception); 30 | $response = [ 31 | 'Outputs' => NULL, 32 | 'ReturnValue' => NULL, 33 | 'Logs' => $this->getLogs() 34 | ]; 35 | header("Content-type: application/json"); 36 | echo(json_encode($response)); 37 | exit(1); 38 | } 39 | 40 | public function route() { 41 | $requestUri = $_SERVER['REQUEST_URI']; 42 | $requestBody = file_get_contents('php://input'); 43 | $request = json_decode($requestBody, true); 44 | 45 | if (is_array($request) && array_key_exists('Data', $request)) { 46 | $this->context->inputs = $request['Data']; 47 | } 48 | 49 | // ob_start(); 50 | 51 | // TODO check if file exists, if it doesn't throw exception, else load. 52 | $handler = $this->baseFunctionsDir . $requestUri . '/index.php'; 53 | if (file_exists($handler)) { 54 | require_once($handler); 55 | $returnValue = run($this->context); 56 | } else { 57 | $returnValue = sprintf("No Handler found for specified path: %s.", $requestUri); 58 | } 59 | 60 | // $functionOut = ob_get_contents(); 61 | 62 | // $this->log->info($functionOut); 63 | 64 | // ob_end_clean(); 65 | 66 | if (is_null($returnValue)) { 67 | $returnValue = ""; 68 | } 69 | 70 | return [ 71 | 'Outputs' => $this->context->outputs, 72 | 'ReturnValue' => $returnValue, 73 | 'Logs' => $this->getLogs() 74 | ]; 75 | } 76 | 77 | private function getLogs() { 78 | $logs = []; 79 | $formatter = new LineFormatter("[%datetime%] %channel%.%level_name%: %message%"); 80 | foreach($this->logHandler->getRecords() as $record) { 81 | $logs[] = $formatter->format($record); 82 | } 83 | return $logs; 84 | } 85 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Serverless # 2 | 3 | This projects provides a PHP [Custom Handler](https://docs.microsoft.com/azure/azure-functions/functions-custom-handlers?WT.mc_id=data-11039-alvidela) for Azure Functions. 4 | 5 | ## Installation ## 6 | 7 | In your composer.json add the following dependency: 8 | 9 | ```json 10 | "require": { 11 | "videlalvaro/azserverless": "*" 12 | } 13 | ``` 14 | 15 | Then run: 16 | 17 | ```bash 18 | composer update 19 | ``` 20 | 21 | Then start an Azure Functions project. In your `host.json` add the following: 22 | 23 | ```json 24 | "customHandler": { 25 | "description": { 26 | "defaultExecutablePath": "php", 27 | "arguments": [ 28 | "-S", 29 | "0.0.0.0:%FUNCTIONS_CUSTOMHANDLER_PORT%", 30 | "vendor/videlalvaro/azserverless/bin/serverless.php" 31 | ] 32 | }, 33 | "enableForwardingHttpRequest": false 34 | }, 35 | ``` 36 | 37 | See `host.sample.json` for an example of how this file should look like. 38 | 39 | Finally, make copy the file `local.settings.sample.json` into your project and call it `local.settings.json`. Adapt the contents to include your connection strings in the `AzureWebJobsStorage` field. 40 | 41 | ## Usage ## 42 | 43 | In your Azure Functions project you will have one folder per serverless function. 44 | 45 | To create a function called `HttpTrigger`, create a folder with the same name, then inside add two files: `function.json` and `index.php`. Here's their content: 46 | 47 | ```json 48 | { 49 | "disabled": false, 50 | "bindings": [ 51 | { 52 | "authLevel": "anonymous", 53 | "type": "httpTrigger", 54 | "direction": "in", 55 | "name": "req" 56 | }, 57 | { 58 | "type": "http", 59 | "direction": "out", 60 | "name": "$return" 61 | } 62 | ] 63 | } 64 | ``` 65 | 66 | And the corresponding index.php file: 67 | 68 | ```php 69 | inputs['req']; 75 | 76 | $context->log->info('Http trigger invoked'); 77 | 78 | $query = json_decode($req['Query'], true); 79 | 80 | if (array_key_exists('name', $query)) { 81 | $message = 'Hello ' . $query['name'] . '!'; 82 | } else { 83 | $message = 'Please pass a name in the query string'; 84 | } 85 | 86 | return [ 87 | 'body' => $message, 88 | 'headers' => [ 89 | 'Content-type' => 'text/plain' 90 | ] 91 | ]; 92 | } 93 | ?> 94 | ``` 95 | 96 | As you can see functions are provided with a `FunctionContext` object where they can access `request` data, and also log information to the console. 97 | 98 | To learn more about the details of serverless on Azure, take a look at the [Azure Functions documentation](https://docs.microsoft.com/azure/azure-functions/create-first-function-vs-code-node?WT.mc_id=data-11039-alvidela). 99 | 100 | ## Deployment ## 101 | 102 | Follow the instructions here to enable PHP 7.4 & make sure composer is run while deploying your function app to Azure: [Configure a PHP app for Azure App Service](https://docs.microsoft.com/azure/app-service/configure-language-php?pivots=platform-windows&WT.mc_id=data-11039-alvidela#set-php-version) 103 | 104 | When the options for Azure CLI require a `--name` option, provide your Azure Functions app name. 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alvaro.videla@microsoft.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | 77 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "b42540ef5d191fa36a576fba0e3c4ae9", 8 | "packages": [ 9 | { 10 | "name": "monolog/monolog", 11 | "version": "dev-master", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Seldaek/monolog.git", 15 | "reference": "39637a5d0e98068d98189ce48a87f3dd61455429" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/39637a5d0e98068d98189ce48a87f3dd61455429", 20 | "reference": "39637a5d0e98068d98189ce48a87f3dd61455429", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.2", 25 | "psr/log": "^1.0.1" 26 | }, 27 | "provide": { 28 | "psr/log-implementation": "1.0.0" 29 | }, 30 | "require-dev": { 31 | "aws/aws-sdk-php": "^2.4.9 || ^3.0", 32 | "doctrine/couchdb": "~1.0@dev", 33 | "elasticsearch/elasticsearch": "^6.0", 34 | "graylog2/gelf-php": "^1.4.2", 35 | "php-amqplib/php-amqplib": "~2.4", 36 | "php-console/php-console": "^3.1.3", 37 | "php-parallel-lint/php-parallel-lint": "^1.0", 38 | "phpspec/prophecy": "^1.6.1", 39 | "phpunit/phpunit": "^8.5", 40 | "predis/predis": "^1.1", 41 | "rollbar/rollbar": "^1.3", 42 | "ruflin/elastica": ">=0.90 <3.0", 43 | "swiftmailer/swiftmailer": "^5.3|^6.0" 44 | }, 45 | "suggest": { 46 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 47 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 48 | "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", 49 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 50 | "ext-mbstring": "Allow to work properly with unicode symbols", 51 | "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", 52 | "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", 53 | "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", 54 | "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", 55 | "php-console/php-console": "Allow sending log messages to Google Chrome", 56 | "rollbar/rollbar": "Allow sending log messages to Rollbar", 57 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server" 58 | }, 59 | "default-branch": true, 60 | "type": "library", 61 | "extra": { 62 | "branch-alias": { 63 | "dev-master": "2.x-dev" 64 | } 65 | }, 66 | "autoload": { 67 | "psr-4": { 68 | "Monolog\\": "src/Monolog" 69 | } 70 | }, 71 | "notification-url": "https://packagist.org/downloads/", 72 | "license": [ 73 | "MIT" 74 | ], 75 | "authors": [ 76 | { 77 | "name": "Jordi Boggiano", 78 | "email": "j.boggiano@seld.be", 79 | "homepage": "https://seld.be" 80 | } 81 | ], 82 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 83 | "homepage": "https://github.com/Seldaek/monolog", 84 | "keywords": [ 85 | "log", 86 | "logging", 87 | "psr-3" 88 | ], 89 | "support": { 90 | "issues": "https://github.com/Seldaek/monolog/issues", 91 | "source": "https://github.com/Seldaek/monolog/tree/master" 92 | }, 93 | "funding": [ 94 | { 95 | "url": "https://github.com/Seldaek", 96 | "type": "github" 97 | }, 98 | { 99 | "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", 100 | "type": "tidelift" 101 | } 102 | ], 103 | "time": "2020-10-29T15:43:01+00:00" 104 | }, 105 | { 106 | "name": "psr/log", 107 | "version": "dev-master", 108 | "source": { 109 | "type": "git", 110 | "url": "https://github.com/php-fig/log.git", 111 | "reference": "dd738d0b4491f32725492cf345f6b501f5922fec" 112 | }, 113 | "dist": { 114 | "type": "zip", 115 | "url": "https://api.github.com/repos/php-fig/log/zipball/dd738d0b4491f32725492cf345f6b501f5922fec", 116 | "reference": "dd738d0b4491f32725492cf345f6b501f5922fec", 117 | "shasum": "" 118 | }, 119 | "require": { 120 | "php": ">=5.3.0" 121 | }, 122 | "default-branch": true, 123 | "type": "library", 124 | "extra": { 125 | "branch-alias": { 126 | "dev-master": "1.1.x-dev" 127 | } 128 | }, 129 | "autoload": { 130 | "psr-4": { 131 | "Psr\\Log\\": "Psr/Log/" 132 | } 133 | }, 134 | "notification-url": "https://packagist.org/downloads/", 135 | "license": [ 136 | "MIT" 137 | ], 138 | "authors": [ 139 | { 140 | "name": "PHP-FIG", 141 | "homepage": "https://www.php-fig.org/" 142 | } 143 | ], 144 | "description": "Common interface for logging libraries", 145 | "homepage": "https://github.com/php-fig/log", 146 | "keywords": [ 147 | "log", 148 | "psr", 149 | "psr-3" 150 | ], 151 | "support": { 152 | "source": "https://github.com/php-fig/log/tree/master" 153 | }, 154 | "time": "2020-09-18T06:44:51+00:00" 155 | } 156 | ], 157 | "packages-dev": [], 158 | "aliases": [], 159 | "minimum-stability": "dev", 160 | "stability-flags": [], 161 | "prefer-stable": false, 162 | "prefer-lowest": false, 163 | "platform": { 164 | "php": ">=7.2", 165 | "ext-json": "*" 166 | }, 167 | "platform-dev": [], 168 | "plugin-api-version": "2.0.0" 169 | } 170 | --------------------------------------------------------------------------------