├── LICENSE ├── README.md ├── composer.json └── src ├── Backtrace ├── Backtrace.php ├── Context.php ├── Normalizer.php ├── SkipInternal.php └── Xdebug.php ├── Container ├── Container.php ├── ServiceProviderInterface.php └── Utility.php ├── CurlHttpMessage ├── AbstractClient.php ├── Client.php ├── ClientAsync.php ├── CurlReqRes.php ├── CurlReqResOptions.php ├── Exception │ ├── BadResponseException.php │ ├── NetworkException.php │ └── RequestException.php ├── Factory.php ├── Handler │ ├── Curl.php │ ├── CurlMulti.php │ └── Mock.php ├── HandlerStack.php └── Middleware │ ├── FollowLocation.php │ └── Status.php ├── Debug ├── AbstractComponent.php ├── AbstractDebug.php ├── Abstraction │ ├── AbstractArray.php │ ├── AbstractObject.php │ ├── AbstractString.php │ ├── Abstracter.php │ ├── Abstraction.php │ ├── Object │ │ ├── AbstractInheritable.php │ │ ├── Abstraction.php │ │ ├── Constants.php │ │ ├── Definition.php │ │ ├── Helper.php │ │ ├── MethodParams.php │ │ ├── Methods.php │ │ ├── Properties.php │ │ ├── PropertiesDom.php │ │ ├── PropertiesInstance.php │ │ ├── PropertiesPhpDoc.php │ │ └── Subscriber.php │ └── Type.php ├── AssetProviderInterface.php ├── Autoloader.php ├── Collector │ ├── AbstractAsyncMiddleware.php │ ├── CurlHttpMessageMiddleware.php │ ├── DatabaseTrait.php │ ├── Doctrine │ │ ├── Connection.php │ │ ├── Driver.php │ │ ├── LoggerCompatTrait.php │ │ ├── LoggerCompatTrait_legacy.php │ │ ├── LoggerCompatTrait_php8.4.php │ │ └── Statement.php │ ├── DoctrineLogger.php │ ├── DoctrineMiddleware.php │ ├── GuzzleMiddleware.php │ ├── MonologHandler.php │ ├── MonologHandler │ │ ├── CompatTrait.php │ │ ├── CompatTrait_2.0.php │ │ └── CompatTrait_3.0.php │ ├── MySqli.php │ ├── MySqli │ │ ├── ExecuteQueryTrait.php │ │ ├── ExecuteQueryTrait_php8.2.php │ │ └── MySqliStmt.php │ ├── OAuth.php │ ├── Pdo.php │ ├── Pdo │ │ ├── CompatTrait.php │ │ ├── CompatTrait_php5.6.php │ │ └── Statement.php │ ├── PhpCurlClass.php │ ├── SimpleCache.php │ ├── SimpleCache │ │ ├── CallInfo.php │ │ ├── CompatTrait.php │ │ ├── CompatTrait_1.php │ │ ├── CompatTrait_2.php │ │ └── CompatTrait_3.php │ ├── SoapClient.php │ ├── StatementInfo.php │ ├── StatementInfoLogger.php │ ├── SwiftMailerLogger.php │ └── TwigExtension.php ├── Config.php ├── ConfigurableInterface.php ├── Data.php ├── Debug.php ├── Dump │ ├── AbstractValue.php │ ├── Base.php │ ├── Base │ │ ├── BaseObject.php │ │ └── Value.php │ ├── Html.php │ ├── Html │ │ ├── Group.php │ │ ├── Helper.php │ │ ├── HtmlArray.php │ │ ├── HtmlObject.php │ │ ├── HtmlString.php │ │ ├── HtmlStringBinary.php │ │ ├── HtmlStringEncoded.php │ │ ├── Object │ │ │ ├── AbstractSection.php │ │ │ ├── Cases.php │ │ │ ├── Constants.php │ │ │ ├── ExtendsImplements.php │ │ │ ├── Methods.php │ │ │ ├── PhpDoc.php │ │ │ └── Properties.php │ │ ├── Table.php │ │ └── Value.php │ ├── Substitution.php │ ├── Text.php │ ├── Text │ │ ├── TextObject.php │ │ └── Value.php │ ├── TextAnsi.php │ ├── TextAnsi │ │ ├── TextAnsiObject.php │ │ └── Value.php │ └── charData.php ├── Framework │ ├── Cake4.php │ ├── Laravel │ │ ├── CacheEventsSubscriber.php │ │ ├── EventServiceProvider.php │ │ ├── EventsSubscriber.php │ │ ├── LogDb.php │ │ ├── LogViews.php │ │ ├── Middleware.php │ │ ├── ServiceProvider.php │ │ └── config.php │ ├── Slim2.php │ ├── Symfony │ │ └── DebugBundle │ │ │ ├── BdkDebugBundle.php │ │ │ ├── DependencyInjection │ │ │ └── BdkDebugExtension.php │ │ │ ├── EventListener │ │ │ └── BdkDebugBundleListener.php │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ └── Resources │ │ │ └── config │ │ │ └── config.yml │ ├── Yii1_1 │ │ ├── Component.php │ │ ├── ErrorLogger.php │ │ ├── EventSubscribers.php │ │ ├── LogRoute.php │ │ ├── LogRouteMeta.php │ │ ├── PdoCollector.php │ │ └── UserInfo.php │ └── Yii2 │ │ ├── CollectEvents.php │ │ ├── EventSubscribers.php │ │ ├── LogTarget.php │ │ ├── LogUser.php │ │ └── Module.php ├── LogEntry.php ├── Plugin │ ├── AbstractLogReqRes.php │ ├── AssertSettingTrait.php │ ├── Channel.php │ ├── ConfigEvents.php │ ├── CustomMethodTrait.php │ ├── Highlight.php │ ├── InternalEvents.php │ ├── LogEnv.php │ ├── LogFiles.php │ ├── LogPhp.php │ ├── LogRequest.php │ ├── LogResponse.php │ ├── Manager.php │ ├── Method │ │ ├── Alert.php │ │ ├── Basic.php │ │ ├── Clear.php │ │ ├── Count.php │ │ ├── General.php │ │ ├── Group.php │ │ ├── GroupCleanup.php │ │ ├── GroupStack.php │ │ ├── Output.php │ │ ├── Profile.php │ │ ├── ReqRes.php │ │ ├── Table.php │ │ ├── Time.php │ │ └── Trace.php │ ├── Prettify.php │ ├── Redaction.php │ ├── Route.php │ └── Runtime.php ├── PluginInterface.php ├── Psr15 │ └── Middleware.php ├── Psr3 │ ├── CompatTrait.php │ ├── CompatTrait_2.php │ ├── CompatTrait_3.php │ └── Logger.php ├── Route │ ├── AbstractErrorRoute.php │ ├── AbstractRoute.php │ ├── ChromeLogger.php │ ├── Discord.php │ ├── Email.php │ ├── Firephp.php │ ├── Html.php │ ├── Html │ │ ├── ErrorSummary.php │ │ ├── FatalError.php │ │ └── Tabs.php │ ├── RouteInterface.php │ ├── Script.php │ ├── ServerLog.php │ ├── Slack.php │ ├── Stream.php │ ├── Teams.php │ ├── Text.php │ ├── Wamp.php │ ├── WampCrate.php │ └── WampHelper.php ├── ServiceProvider.php ├── Utility │ ├── ArrayUtil.php │ ├── ArrayUtilHelperTrait.php │ ├── ErrorLevel.php │ ├── FileStreamWrapper.php │ ├── FileStreamWrapperBase.php │ ├── FileTree.php │ ├── FindExit.php │ ├── Html.php │ ├── HtmlBuild.php │ ├── HtmlParse.php │ ├── HtmlSanitize.php │ ├── Php.php │ ├── PhpDoc.php │ ├── PhpDoc │ │ ├── Helper.php │ │ ├── ParseMethod.php │ │ ├── ParseParam.php │ │ ├── Parsers.php │ │ └── Type.php │ ├── Profile.php │ ├── Reflection.php │ ├── SerializeLog.php │ ├── Sql.php │ ├── SqlQueryAnalysis.php │ ├── StopWatch.php │ ├── StringUtil.php │ ├── StringUtilHelperTrait.php │ ├── Table.php │ ├── TableRow.php │ ├── UseStatements.php │ ├── Utf8.php │ ├── Utf8Buffer.php │ └── Utility.php ├── css │ ├── Debug.css │ └── PrismJsLightDark.css └── js │ ├── Debug.jquery.js │ ├── Debug.jquery.min.js │ ├── prism.css │ └── prism.js ├── ErrorHandler ├── AbstractComponent.php ├── AbstractError.php ├── AbstractErrorHandler.php ├── Error.php ├── ErrorHandler.php └── Plugin │ ├── Emailer.php │ ├── Stats.php │ ├── StatsStoreFile.php │ └── StatsStoreInterface.php ├── Promise ├── Coroutine.php ├── Create.php ├── Each.php ├── EachPromise.php ├── Exception │ ├── AggregateException.php │ ├── CancellationException.php │ └── RejectionException.php ├── FulfilledPromise.php ├── Is.php ├── Promise.php ├── PromiseInterface.php ├── RejectedPromise.php ├── TaskQueue.php └── Utils.php ├── PubSub ├── AbstractManager.php ├── Event.php ├── InterfaceManager.php ├── Manager.php ├── SubscriberInterface.php └── ValueStore.php ├── Slack ├── AbstractBlockFactory.php ├── AbstractSlack.php ├── AssertionTrait.php ├── BlockElementsFactory.php ├── BlockFactory.php ├── SlackApi.php ├── SlackCommand.php ├── SlackMessage.php └── SlackWebhook.php └── Teams ├── AbstractExtendableItem.php ├── AbstractItem.php ├── Actions ├── AbstractAction.php ├── ActionInterface.php ├── OpenUrl.php └── ShowCard.php ├── CardUtilityTrait.php ├── Cards ├── AbstractCard.php ├── AdaptiveCard.php ├── CardInterface.php ├── HeroCard.php └── MessageCard.php ├── Elements ├── AbstractElement.php ├── AbstractToggleableItem.php ├── ActionSet.php ├── Column.php ├── ColumnSet.php ├── CommonTrait.php ├── Container.php ├── ElementInterface.php ├── Fact.php ├── FactSet.php ├── Image.php ├── Media.php ├── MediaSource.php ├── RichTextBlock.php ├── Table.php ├── TableCell.php ├── TableRow.php ├── TextBlock.php └── TextRun.php ├── Enums.php ├── ItemInterface.php ├── Section.php └── TeamsWebhook.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Brad Kent 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. 22 | 23 | Includes other software released under the MIT license: 24 | - PrismJS/prism, Copyright 2012 Lea Verou 25 | - psr/http-message, Copyright 2014 PHP Framework Interoperability Group 26 | 27 | Portions of this software derived from other works 28 | - https://github.com/silexphp/Pimple 29 | - https://github.com/symfony/event-dispatcher 30 | - https://github.com/guzzle/guzzle 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPDebugConsole 2 | 3 | Browser/javascript like console class for PHP 4 | 5 | Log, Debug, Inspect 6 | 7 | **Website/Usage/Examples:** 8 | 9 | * PHP port of the [javascript web console api](https://developer.mozilla.org/en-US/docs/Web/API/console) 10 | * multiple simultaneous output options 11 | * [ChromeLogger](https://craig.is/writing/chrome-logger/techspecs) 12 | * [FirePHP](http://www.firephp.org/) (no FirePHP dependency!) 13 | * HTML 14 | * Plain text / file 15 | * <script> 16 | * WebSocket (WAMP) 17 | * "plugin" 18 | * "Collectors" / wrappers for 19 | * Guzzle 20 | * Doctrine 21 | * Mysqli 22 | * PDO 23 | * PhpCurlClass 24 | * SimpleCache 25 | * SoapClient 26 | * SwiftMailer 27 | * more 28 | * [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) (Logger) Implementation 29 | * [PSR-15](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-15-request-handlers-meta.md) (Middleware) Implementation 30 | * custom error handler 31 | * errors (even fatal) are captured / logged / displayed 32 | * optionally send error notices via email (throttled as to not to send out a flood of emails) 33 | * password protected 34 | * send debug log via email 35 | 36 | ![Screenshot of PHPDebugConsole's Output](http://www.bradkent.com/images/php/screenshot_1.4.png) 37 | 38 | ## Installation 39 | 40 | This library supports PHP 5.4 - 8.4 41 | 42 | It is installable and autoloadable via [Composer](https://getcomposer.org/) as [bdk/debug](https://packagist.org/packages/bdk/debug). 43 | 44 | ```json 45 | { 46 | "require": { 47 | "bdk/debug": "^3.4", 48 | } 49 | } 50 | ``` 51 | 52 | **installation without Composer** 53 | 54 | As of v3.3 this is no longer officially supported due to now requiring one or more dependencies. 55 | 56 | ## Usage 57 | 58 | See 59 | 60 | ## PSR-3 Usage 61 | 62 | PHPDebugConsole includes a [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) implementation (which can be used as a [monolog](https://github.com/Seldaek/monolog) PSR handler). If you're using a application or library that uses these standards, drop PHPDebugConsole right in. 63 | 64 | (this library includes neither psr/log or monolog/monolog. Include separately if needed.) 65 | 66 | PSR-3: 67 | 68 | ```php 69 | // instantiate PHPDebugLogger / get instance 70 | $debug = \bdk\Debug::getInstance(); 71 | $psr3logger = $debug->logger; 72 | $psr3logger->emergency('fallen and can\'t get up'); 73 | ``` 74 | 75 | monolog: 76 | 77 | ```php 78 | $monolog = new \Monolog\Logger('myApplication'); 79 | $monolog->pushHandler(new \bdk\Debug\Collector\MonologHandler($debug)); 80 | $monolog->critical('all your base are belong to them'); 81 | ``` 82 | 83 | ## Methods 84 | 85 | * log 86 | * info 87 | * warn 88 | * error 89 | * assert 90 | * clear 91 | * count 92 | * countReset 93 | * group 94 | * groupCollapsed 95 | * groupEnd 96 | * profile 97 | * profileEnd 98 | * table 99 | * time 100 | * timeEnd 101 | * timeLog 102 | * trace 103 | * *… [more](http://www.bradkent.com/php/debug#methods)* 104 | 105 | ## Tests / Quality 106 | 107 | ![Supported PHP versions](https://img.shields.io/static/v1?label=PHP&message=5.4%20-%208.4&color=blue) 108 | ![Build Status](https://img.shields.io/github/actions/workflow/status/bkdotcom/PHPDebugConsole/phpunit.yml.svg?branch=master&logo=github) 109 | [![Codacy Score](https://img.shields.io/codacy/grade/e950849edfd9463b993386080d39875e/master.svg?logo=codacy)](https://app.codacy.com/gh/bkdotcom/PHPDebugConsole/dashboard) 110 | [![Maintainability](https://img.shields.io/codeclimate/maintainability/bkdotcom/PHPDebugConsole.svg?logo=codeclimate)](https://codeclimate.com/github/bkdotcom/PHPDebugConsole) 111 | [![Coverage](https://img.shields.io/codeclimate/coverage-letter/bkdotcom/PHPDebugConsole.svg?logo=codeclimate)](https://codeclimate.com/github/bkdotcom/PHPDebugConsole) 112 | 113 | ## Changelog 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/Container/ServiceProviderInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since v3.0 11 | */ 12 | 13 | namespace bdk\Container; 14 | 15 | use bdk\Container; 16 | 17 | /** 18 | * Container 19 | */ 20 | interface ServiceProviderInterface 21 | { 22 | /** 23 | * Registers services, factories, & values on the given container. 24 | * 25 | * This method should only be used to configure services and parameters. 26 | * It should not get services. 27 | * 28 | * @param Container $container Container instance 29 | * 30 | * @return void 31 | */ 32 | public function register(Container $container); 33 | } 34 | -------------------------------------------------------------------------------- /src/Container/Utility.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since v3.1 11 | */ 12 | 13 | namespace bdk\Container; 14 | 15 | use bdk\Container; 16 | use bdk\Container\ServiceProviderInterface; 17 | use InvalidArgumentException; 18 | 19 | /** 20 | * Container utilities 21 | */ 22 | class Utility 23 | { 24 | /** 25 | * Get the container's raw values 26 | * 27 | * @param Container $container Container instance 28 | * 29 | * @return array 30 | */ 31 | public static function getRawValues(Container $container) 32 | { 33 | $keys = $container->keys(); 34 | $return = array(); 35 | foreach ($keys as $key) { 36 | $return[$key] = $container->raw($key); 37 | } 38 | return $return; 39 | } 40 | 41 | /** 42 | * Get values from Container, ServiceProviderInterface, callable or plain array 43 | * 44 | * @param Container|ServiceProviderInterface|callable|array $val dependency definitions 45 | * 46 | * @return array 47 | * 48 | * @throws InvalidArgumentException 49 | */ 50 | public static function toRawValues($val) 51 | { 52 | if ($val instanceof Container) { 53 | return self::getRawValues($val); 54 | } 55 | if ($val instanceof ServiceProviderInterface) { 56 | $container = new Container(); 57 | $container->registerProvider($val); 58 | return self::getRawValues($container); 59 | } 60 | if (\is_callable($val)) { 61 | $container = new Container(); 62 | \call_user_func($val, $container); 63 | return self::getRawValues($container); 64 | } 65 | if (\is_array($val)) { 66 | return $val; 67 | } 68 | throw new InvalidArgumentException(\sprintf( 69 | 'toRawValues expects Container, ServiceProviderInterface, callable, or key->value array. %s provided', 70 | self::getDebugType($val) 71 | )); 72 | } 73 | 74 | /** 75 | * Assert that the identifier exists 76 | * 77 | * @param mixed $val Value to check 78 | * 79 | * @return void 80 | * 81 | * @throws InvalidArgumentException If the identifier is not defined 82 | */ 83 | public static function assertInvokable($val) 84 | { 85 | if (\is_object($val) === false || \method_exists($val, '__invoke') === false) { 86 | throw new InvalidArgumentException(\sprintf( 87 | 'Closure or invokable object expected. %s provided', 88 | self::getDebugType($val) 89 | )); 90 | } 91 | } 92 | 93 | /** 94 | * Gets the type name of a variable in a way that is suitable for debugging 95 | * 96 | * @param mixed $value Value to inspect 97 | * 98 | * @return string 99 | */ 100 | protected static function getDebugType($value) 101 | { 102 | return \is_object($value) 103 | ? \get_class($value) 104 | : \strtolower(\gettype($value)); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/CurlHttpMessage/Exception/BadResponseException.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\CurlHttpMessage\Exception; 11 | 12 | use bdk\CurlHttpMessage\Exception\RequestException; 13 | 14 | /** 15 | * Exception when an HTTP error occurs (4xx or 5xx error) 16 | */ 17 | class BadResponseException extends RequestException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/CurlHttpMessage/Exception/NetworkException.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\CurlHttpMessage\Exception; 11 | 12 | use bdk\CurlHttpMessage\Exception\RequestException; 13 | 14 | /** 15 | * Network Exception 16 | * 17 | * Failed http request due to a network related issue 18 | */ 19 | class NetworkException extends RequestException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/CurlHttpMessage/Handler/Curl.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\CurlHttpMessage\Handler; 11 | 12 | use bdk\CurlHttpMessage\CurlReqRes; 13 | use bdk\Promise\FulfilledPromise; 14 | use bdk\Promise\PromiseInterface; 15 | use CurlHandle; 16 | 17 | /** 18 | * Fetch the request with curl, and return a Promise 19 | */ 20 | class Curl 21 | { 22 | /** @var CurlHandle|resource */ 23 | protected $curlHandle; 24 | 25 | /** 26 | * Invoke handler 27 | * 28 | * @param CurlReqRes $curlReqRes CurlReqRes instance 29 | * 30 | * @return PromiseInterface 31 | */ 32 | public function __invoke(CurlReqRes $curlReqRes) 33 | { 34 | $curlHandle = $this->getCurlHandle(); 35 | $curlReqRes->setCurlHandle($curlHandle); 36 | $response = $curlReqRes->exec(); 37 | return new FulfilledPromise($response); 38 | } 39 | 40 | /** 41 | * Reuse existing curl handle or init a new one 42 | * 43 | * @return resource|CurlHandle 44 | */ 45 | private function getCurlHandle() 46 | { 47 | if (\is_resource($this->curlHandle) === false && ($this->curlHandle instanceof CurlHandle) === false) { 48 | $this->curlHandle = \curl_init(); 49 | return $this->curlHandle; 50 | } 51 | if (\function_exists('curl_reset') === false) { 52 | \curl_close($this->curlHandle); 53 | $this->curlHandle = \curl_init(); 54 | return $this->curlHandle; 55 | } 56 | \curl_reset($this->curlHandle); 57 | return $this->curlHandle; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/CurlHttpMessage/Middleware/Status.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\CurlHttpMessage\Middleware; 11 | 12 | use bdk\CurlHttpMessage\CurlReqRes; 13 | use bdk\CurlHttpMessage\Exception\BadResponseException; 14 | use Psr\Http\Message\ResponseInterface; 15 | 16 | /** 17 | * Check response for 4xx or 5xx error 18 | */ 19 | class Status 20 | { 21 | /** 22 | * Invoke 23 | * 24 | * @param callable $handler Next request handler in the middleware stack 25 | * 26 | * @return Closure 27 | */ 28 | public function __invoke(callable $handler) 29 | { 30 | return static function (CurlReqRes $curlReqRes) use ($handler) { 31 | return $handler($curlReqRes) 32 | ->then(static function (ResponseInterface $response) use ($curlReqRes) { 33 | $code = $response->getStatusCode(); 34 | if ($code < 400) { 35 | return $response; 36 | } 37 | throw BadResponseException::create($curlReqRes->getRequest(), $response); 38 | }); 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Debug/AbstractComponent.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug; 14 | 15 | use bdk\Debug\ConfigurableInterface; 16 | use bdk\ErrorHandler\AbstractComponent as BaseAbstractComponent; 17 | 18 | /** 19 | * Base "component" methods 20 | */ 21 | abstract class AbstractComponent extends BaseAbstractComponent implements ConfigurableInterface 22 | { 23 | /** @var callable */ 24 | protected $setCfgMergeCallable = 'array_merge'; 25 | } 26 | -------------------------------------------------------------------------------- /src/Debug/AssetProviderInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug; 14 | 15 | /** 16 | * Provide css and/or javascript assets 17 | */ 18 | interface AssetProviderInterface 19 | { 20 | /** 21 | * Returns an array with the following keys: 22 | * * css: filename, css, or array thereof 23 | * * script: filename, javascript, or array thereof 24 | * 25 | * @return array 26 | */ 27 | public function getAssets(); 28 | } 29 | -------------------------------------------------------------------------------- /src/Debug/Autoloader.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug; 14 | 15 | /** 16 | * PHPDebugConsole autoloader 17 | */ 18 | class Autoloader 19 | { 20 | /** @var array */ 21 | protected $classMap = array(); 22 | 23 | /** @var array */ 24 | protected $psr4Map = array(); 25 | 26 | /** 27 | * Register autoloader 28 | * 29 | * @return bool 30 | */ 31 | public function register() 32 | { 33 | $this->classMap = array( 34 | 'bdk\\Backtrace' => __DIR__ . '/../Backtrace/Backtrace.php', 35 | 'bdk\\Container' => __DIR__ . '/../Container/Container.php', 36 | 'bdk\\Debug' => __DIR__ . '/Debug.php', 37 | 'bdk\\Debug\\Utility' => __DIR__ . '/Utility/Utility.php', 38 | 'bdk\\ErrorHandler' => __DIR__ . '/../ErrorHandler/ErrorHandler.php', 39 | 'bdk\\Promise' => __DIR__ . '/../Promise/Promise.php', 40 | ); 41 | $this->psr4Map = array( 42 | 'bdk\\Backtrace\\' => __DIR__ . '/../Backtrace', 43 | 'bdk\\Container\\' => __DIR__ . '/../Container', 44 | 'bdk\\CurlHttpMessage\\' => __DIR__ . '/../CurlHttpMessage', 45 | 'bdk\\Debug\\' => __DIR__, 46 | 'bdk\\ErrorHandler\\' => __DIR__ . '/../ErrorHandler', 47 | 'bdk\\Promise\\' => __DIR__ . '/../Promise', 48 | 'bdk\\PubSub\\' => __DIR__ . '/../PubSub', 49 | 'bdk\\Slack\\' => __DIR__ . '/../Slack', 50 | 'bdk\\Teams\\' => __DIR__ . '/../Teams', 51 | 'bdk\\Test\\Debug\\' => __DIR__ . '/../../tests/Debug', 52 | ); 53 | return \spl_autoload_register([$this, 'autoload']); 54 | } 55 | 56 | /** 57 | * Remove autoloader 58 | * 59 | * @return bool 60 | */ 61 | public function unregister() 62 | { 63 | return \spl_autoload_unregister([$this, 'autoload']); 64 | } 65 | 66 | /** 67 | * Add classname to classMap 68 | * 69 | * @param string $className ClassName 70 | * @param string $filepath Filepath to class' definition 71 | * 72 | * @return static 73 | */ 74 | public function addClass($className, $filepath) 75 | { 76 | $this->classMap[$className] = $filepath; 77 | return $this; 78 | } 79 | 80 | /** 81 | * Add Psr4 mapping to autoloader 82 | * 83 | * @param string $namespace Namespace prefix 84 | * @param string $dir Directory containing namespace 85 | * 86 | * @return static 87 | */ 88 | public function addPsr4($namespace, $dir) 89 | { 90 | $this->psr4Map[$namespace] = $dir; 91 | return $this; 92 | } 93 | 94 | /** 95 | * Debug class autoloader 96 | * 97 | * @param string $className classname to attempt to load 98 | * 99 | * @return void 100 | */ 101 | protected function autoload($className) 102 | { 103 | $className = \ltrim($className, '\\'); // leading backslash _shouldn't_ have been passed 104 | $filepath = $this->findClass($className); 105 | if ($filepath) { 106 | require $filepath; 107 | } 108 | } 109 | 110 | /** 111 | * Find file containing class 112 | * 113 | * @param string $className classname to find 114 | * 115 | * @return string|false 116 | */ 117 | private function findClass($className) 118 | { 119 | if (isset($this->classMap[$className])) { 120 | return $this->classMap[$className]; 121 | } 122 | foreach ($this->psr4Map as $namespace => $dir) { 123 | if (\strpos($className, $namespace) === 0) { 124 | $rel = \substr($className, \strlen($namespace)); 125 | $rel = \str_replace('\\', '/', $rel); 126 | return $dir . '/' . $rel . '.php'; 127 | } 128 | } 129 | return false; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Debug/Collector/CurlHttpMessageMiddleware.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector; 14 | 15 | use bdk\CurlHttpMessage\CurlReqRes; 16 | use bdk\CurlHttpMessage\Exception\RequestException; 17 | use bdk\Promise; 18 | use Psr\Http\Message\ResponseInterface; 19 | 20 | /** 21 | * PHPDebugConsole Middleware for CurlHttpMEssage 22 | */ 23 | class CurlHttpMessageMiddleware extends AbstractAsyncMiddleware 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function __construct($cfg = array(), $debug = null) 29 | { 30 | \bdk\Debug\Utility::assertType($debug, 'bdk\Debug'); 31 | 32 | $this->cfg = \array_merge($this->cfg, array( 33 | 'idPrefix' => 'curl_', 34 | 'label' => 'CurlHttpMessage', 35 | )); 36 | parent::__construct($cfg, $debug); 37 | } 38 | 39 | /** 40 | * Log Request Begin 41 | * 42 | * @param CurlReqRes $curlReqRes CurlReqRes instance 43 | * 44 | * @return Promise 45 | */ 46 | public function onRequest(CurlReqRes $curlReqRes) 47 | { 48 | $request = $curlReqRes->getRequest(); 49 | $options = $curlReqRes->getOptions(); 50 | $requestInfo = array( 51 | 'isAsynchronous' => $options['isAsynchronous'], 52 | 'request' => $request, 53 | 'requestId' => \spl_object_hash($request), 54 | ); 55 | $this->onRedirectOrig = $options['onRedirect']; 56 | if ($requestInfo['isAsynchronous'] === false) { 57 | $curlReqRes->setOption('onRedirect', [$this, 'onRedirect']); 58 | } 59 | $this->logRequest($request, $requestInfo); 60 | return $this->doRequest($curlReqRes, $requestInfo); 61 | } 62 | 63 | /** 64 | * Rejected Request handler 65 | * 66 | * @param RequestException $reason Reject reason 67 | * @param array $requestInfo Request information 68 | * 69 | * @return bdk\Promise 70 | */ 71 | public function onRejected(RequestException $reason, array $requestInfo) 72 | { 73 | $meta = $this->debug->meta(); 74 | $response = $reason->getResponse(); 75 | if ($requestInfo['isAsynchronous']) { 76 | $meta = $this->debug->meta(array( 77 | 'asyncResponseGroup' => true, 78 | 'middlewareId' => \spl_object_hash($this), 79 | )); 80 | $this->asyncResponseGroup( 81 | $requestInfo['request'], 82 | $response, 83 | $meta, 84 | true 85 | ); 86 | } 87 | $this->logResponse($response, $requestInfo, $reason); 88 | $this->debug->groupEnd($meta); 89 | return Promise::rejectionFor($reason); 90 | } 91 | 92 | /** 93 | * call nextHandler and register our fulfill and reject callbacks 94 | * 95 | * @param CurlReqRes $curlReqRes CurlReqRes instance 96 | * @param array $requestInfo Request info 97 | * 98 | * @return Promise 99 | */ 100 | protected function doRequest(CurlReqRes $curlReqRes, array $requestInfo) 101 | { 102 | // start timer 103 | $this->debug->time($this->cfg['label'] . ':' . $requestInfo['requestId']); 104 | $handler = $this->nextHandler; 105 | return $handler($curlReqRes)->then( 106 | function (ResponseInterface $response) use ($requestInfo) { 107 | return $this->onFulfilled($response, $requestInfo); 108 | }, 109 | function (RequestException $reason) use ($requestInfo) { 110 | return $this->onRejected($reason, $requestInfo); 111 | } 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Debug/Collector/Doctrine/Driver.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2024-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\Doctrine; 14 | 15 | use bdk\Debug; 16 | use Doctrine\DBAL\Driver as DriverInterface; 17 | use Doctrine\DBAL\Driver\Connection as ConnectionInterface; 18 | use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; 19 | 20 | /** 21 | * Doctrine driver middleware for logging 22 | */ 23 | class Driver extends AbstractDriverMiddleware 24 | { 25 | /** @var Debug */ 26 | protected $debug; 27 | 28 | /** 29 | * Constructor 30 | * 31 | * @param DriverInterface $driver Wrapped Driver instance 32 | * @param Debug $debug Debug instance 33 | */ 34 | public function __construct( 35 | DriverInterface $driver, 36 | Debug $debug 37 | ) 38 | { 39 | parent::__construct($driver); 40 | $this->debug = $debug; 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function connect( 47 | #[\SensitiveParameter] 48 | array $params 49 | ): ConnectionInterface 50 | { 51 | return new Connection( 52 | parent::connect($params), 53 | $params, 54 | $this->debug 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Debug/Collector/Doctrine/LoggerCompatTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2024-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | /* 14 | PHP 8.4 deprecates implicit nullable types 15 | */ 16 | 17 | namespace bdk\Debug\Collector\Doctrine; 18 | 19 | $require = PHP_VERSION_ID >= 80400 20 | ? __DIR__ . '/LoggerCompatTrait_php8.4.php' 21 | : __DIR__ . '/LoggerCompatTrait_legacy.php'; 22 | 23 | require $require; 24 | -------------------------------------------------------------------------------- /src/Debug/Collector/Doctrine/LoggerCompatTrait_legacy.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2024-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\Doctrine; 14 | 15 | use bdk\Debug\Collector\StatementInfo; 16 | 17 | /* 18 | Wrap in condition. 19 | PHPUnit code coverage scans all files and will conflict 20 | */ 21 | if (\trait_exists(__NAMESPACE__ . '\\LoggerCompatTrait', false) === false) { 22 | /** 23 | * Method signature for Php < 8.4 24 | */ 25 | trait LoggerCompatTrait 26 | { 27 | /** 28 | * Logs a SQL statement somewhere. 29 | * 30 | * @param string $sql SQL statement 31 | * @param array|array|null $params Statement parameters 32 | * @param array|array|null $types Parameter types 33 | * 34 | * @return void 35 | * 36 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 37 | */ 38 | public function startQuery($sql, array $params = null, array $types = null) 39 | { 40 | $this->statementInfo = new StatementInfo($sql, $params, $types); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Debug/Collector/Doctrine/LoggerCompatTrait_php8.4.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2024-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\Doctrine; 14 | 15 | use bdk\Debug\Collector\StatementInfo; 16 | 17 | /* 18 | Wrap in condition. 19 | PHPUnit code coverage scans all files and will conflict 20 | */ 21 | if (\trait_exists(__NAMESPACE__ . '\\LoggerCompatTrait', false) === false) { 22 | /** 23 | * Method signature for Php >= 8.4 24 | */ 25 | trait LoggerCompatTrait 26 | { 27 | /** 28 | * Logs a SQL statement somewhere. 29 | * 30 | * @param string $sql SQL statement 31 | * @param array|array|null $params Statement parameters 32 | * @param array|array|null $types Parameter types 33 | * 34 | * @return void 35 | * 36 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 37 | */ 38 | public function startQuery($sql, ?array $params = null, ?array $types = null) 39 | { 40 | $this->statementInfo = new StatementInfo($sql, $params, $types); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Debug/Collector/Doctrine/Statement.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2024-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\Doctrine; 14 | 15 | use bdk\Debug\Collector\Doctrine\Connection; 16 | use bdk\Debug\Collector\StatementInfo; 17 | use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware; 18 | use Doctrine\DBAL\Driver\Result as ResultInterface; 19 | use Doctrine\DBAL\Driver\Statement as StatementInterface; 20 | use Doctrine\DBAL\ParameterType; 21 | 22 | /** 23 | * Statement middleware for logging 24 | */ 25 | class Statement extends AbstractStatementMiddleware 26 | { 27 | /** @var Connection */ 28 | private $connection; 29 | 30 | /** @var string */ 31 | private $sql; 32 | 33 | /** @var array|array */ 34 | private $params = []; 35 | 36 | /** @var array|array */ 37 | private $types = []; 38 | 39 | /** 40 | * Constructor 41 | * 42 | * @param StatementInterface $statement Statement instance 43 | * @param Connection $connection Connection instance (where we're storing logged statements) 44 | * @param string $sql Sql string 45 | */ 46 | public function __construct( 47 | StatementInterface $statement, 48 | Connection $connection, 49 | string $sql 50 | ) 51 | { 52 | parent::__construct($statement); 53 | $this->connection = $connection; 54 | $this->sql = $sql; 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function bindValue($param, $value, $type = ParameterType::STRING): void 61 | { 62 | $this->params[$param] = $value; 63 | $this->types[$param] = $type; 64 | parent::bindValue($param, $value, $type); 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | */ 70 | public function execute($params = null): ResultInterface 71 | { 72 | $info = new StatementInfo($this->sql, $this->params, $this->types); 73 | $result = parent::execute(); 74 | 75 | $exception = null; 76 | $result = false; 77 | $rowCount = null; 78 | try { 79 | $result = parent::execute(); 80 | $rowCount = $result->rowCount(); 81 | } catch (\Exception $e) { 82 | $exception = $e; 83 | } 84 | 85 | $info->end($exception, $rowCount); 86 | $this->connection->addStatementInfo($info); 87 | 88 | if ($exception) { 89 | throw $exception; 90 | } 91 | return $result; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Debug/Collector/DoctrineLogger.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Collector\DatabaseTrait; 17 | use bdk\Debug\Collector\Doctrine\LoggerCompatTrait; 18 | use bdk\Debug\Collector\StatementInfo; 19 | use bdk\PubSub\Event; 20 | use Doctrine\DBAL\Connection; 21 | use Doctrine\DBAL\Logging\SQLLogger as SQLLoggerInterface; 22 | 23 | /** 24 | * Log Doctrine queries 25 | * 26 | * http://doctrine-project.org 27 | * 28 | * Deprecated as of Doctrine 3.2 29 | */ 30 | class DoctrineLogger implements SQLLoggerInterface 31 | { 32 | use DatabaseTrait; 33 | use LoggerCompatTrait; 34 | 35 | /** @var StatementInfo|null */ 36 | protected $statementInfo; 37 | 38 | /** @var Connection */ 39 | private $connection; 40 | 41 | /** 42 | * Constructor 43 | * 44 | * @param Connection|null $connection Optional Doctrine DBAL connection instance 45 | * pass to log connection info 46 | * @param Debug|null $debug Optional DebugInstance 47 | * 48 | * @SuppressWarnings(PHPMD.StaticAccess) 49 | */ 50 | public function __construct($connection = null, $debug = null) 51 | { 52 | \bdk\Debug\Utility::assertType($connection, 'Doctrine\DBAL\Connection'); 53 | \bdk\Debug\Utility::assertType($debug, 'bdk\Debug'); 54 | $this->traitInit($debug, 'Doctrine'); 55 | $this->connection = $connection; 56 | $this->debug->eventManager->subscribe(Debug::EVENT_OUTPUT, [$this, 'onDebugOutput'], 1); 57 | } 58 | 59 | /** 60 | * Debug::EVENT_OUTPUT subscriber 61 | * 62 | * @param Event $event Event instance 63 | * 64 | * @return void 65 | */ 66 | public function onDebugOutput(Event $event) 67 | { 68 | $debug = $event->getSubject(); 69 | $connectionInfo = $this->connection 70 | ? $this->connection->getParams() 71 | : array(); 72 | $debug->groupSummary(0); 73 | $groupParams = \array_filter(array( 74 | 'Doctrine', 75 | $connectionInfo 76 | ? $connectionInfo['url'] 77 | : null, 78 | $this->meta(array( 79 | 'argsAsParams' => false, 80 | 'level' => 'info', 81 | )), 82 | )); 83 | \call_user_func_array([$debug, 'groupCollapsed'], $groupParams); 84 | $this->logRuntime($debug); 85 | $debug->groupEnd(); // groupCollapsed 86 | $debug->groupEnd(); // groupSummary 87 | } 88 | 89 | // startQuery defined in Doctrine\CompatTrait 90 | 91 | /** 92 | * {@inheritDoc} 93 | */ 94 | public function stopQuery() 95 | { 96 | $statementInfo = $this->statementInfo; 97 | $statementInfo->end(); 98 | $this->addStatementInfo($statementInfo); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Debug/Collector/DoctrineMiddleware.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Collector\Doctrine\Driver; 17 | use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware; 18 | use Doctrine\DBAL\Driver as DriverInterface; 19 | use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; 20 | 21 | /** 22 | * Doctrine 3.x requires php 7.3+ 23 | * Doctrine 3.2 introduces middleware for logging 24 | * Doctrine 3.3 adds AbstractXxxxxMiddleware classes (which we use) 25 | * Doctrine 3.4 deprecates SqlLogger 26 | */ 27 | #[AsMiddleware(priority: -10)] 28 | class DoctrineMiddleware implements MiddlewareInterface 29 | { 30 | /** @var Debug */ 31 | protected $debug; 32 | 33 | /** @var string */ 34 | protected $icon = ':database:'; 35 | 36 | /** 37 | * Constructor 38 | * 39 | * @param Debug|null $debug Debug instance 40 | */ 41 | public function __construct(?Debug $debug = null) 42 | { 43 | if (!$debug) { 44 | $debug = Debug::getChannel('Doctrine', array('channelIcon' => $this->icon)); 45 | } elseif ($debug === $debug->rootInstance) { 46 | $debug = $debug->getChannel('Doctrine', array('channelIcon' => $this->icon)); 47 | } 48 | $this->debug = $debug; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | public function wrap(DriverInterface $driver): DriverInterface 55 | { 56 | return new Driver($driver, $this->debug); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Debug/Collector/MonologHandler.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Collector; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Collector\MonologHandler\CompatTrait; 17 | use InvalidArgumentException; 18 | use Monolog\Handler\PsrHandler; 19 | use Monolog\Logger; 20 | use Psr\Log\LoggerInterface; 21 | 22 | /** 23 | * Essentially Monolog's PsrHandler... but passes channel along via context array 24 | */ 25 | class MonologHandler extends PsrHandler 26 | { 27 | use CompatTrait; 28 | 29 | /** 30 | * Constructor 31 | * 32 | * @param Debug|LoggerInterface $debug Debug instance 33 | * @param int $level The minimum logging level at which this handler will be triggered (See Monolog/Logger constants) 34 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not 35 | * 36 | * @throws InvalidArgumentException 37 | * 38 | * @SuppressWarnings(PHPMD.StaticAccess) 39 | */ 40 | public function __construct($debug = null, $level = Logger::DEBUG, $bubble = true) 41 | { 42 | if (!$debug) { 43 | $debug = Debug::getInstance(); 44 | } 45 | $logger = null; 46 | if ($debug instanceof Debug) { 47 | $logger = $debug->logger; 48 | } elseif ($debug instanceof LoggerInterface) { 49 | $logger = $debug; 50 | } 51 | if ($logger === null) { 52 | throw new InvalidArgumentException('$debug must be instanceof bdk\Debug or Psr\Log\LoggerInterface'); 53 | } 54 | parent::__construct($logger, $level, $bubble); 55 | } 56 | 57 | /** 58 | * the `handle` method 59 | * 60 | * Handle method provided by MonologHandlerCompatTrait (to support different method signatures in interface) 61 | * 62 | * @param array $record The record to handle 63 | * 64 | * @return bool true means that this handler handled the record, and that bubbling is not permitted. 65 | * false means the record was either not processed or that this handler allows bubbling. 66 | */ 67 | protected function doHandle(array $record) 68 | { 69 | $this->logger->log( 70 | \strtolower($record['level_name']), 71 | $record['message'], 72 | $record['context'] + array('channel' => $record['channel']) 73 | ); 74 | return $this->bubble === false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Debug/Collector/MonologHandler/CompatTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.5 11 | */ 12 | 13 | namespace bdk\Debug\Collector\MonologHandler; 14 | 15 | use bdk\Debug\Abstraction\Object\Helper as ObjectHelper; 16 | 17 | /* 18 | Support HandlerInterface with/without return type in handle() method definition 19 | */ 20 | 21 | $refClass = new \ReflectionClass('Monolog\\Handler\\HandlerInterface'); 22 | $refMethod = $refClass->getMethod('handle'); 23 | 24 | if (\method_exists($refMethod, 'hasReturnType') && $refMethod->hasReturnType()) { 25 | $refParam = $refMethod->getParameters()[0]; 26 | $type = ObjectHelper::getType(null, $refParam); 27 | require $type === 'array' 28 | ? __DIR__ . '/CompatTrait_2.0.php' 29 | : __DIR__ . '/CompatTrait_3.0.php'; 30 | } elseif (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 31 | /** 32 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 33 | */ 34 | trait CompatTrait 35 | { 36 | /** 37 | * Handles a record. 38 | * 39 | * @param array $record The record to handle 40 | * 41 | * @return bool true means that this handler handled the record, and that bubbling is not permitted. 42 | * false means the record was either not processed or that this handler allows bubbling. 43 | */ 44 | public function handle(array $record) 45 | { 46 | return $this->doHandle($record); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Debug/Collector/MonologHandler/CompatTrait_2.0.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.5 11 | */ 12 | 13 | namespace bdk\Debug\Collector\MonologHandler; 14 | 15 | /* 16 | Wrap in condition. 17 | PHPUnit code coverage scans all files and will conflict 18 | */ 19 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 20 | /** 21 | * Provide handle method (with return type-hint) 22 | * 23 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 24 | */ 25 | trait CompatTrait 26 | { 27 | /** 28 | * Handles a record. 29 | * 30 | * @param array $record The record to handle 31 | * 32 | * @return bool true means that this handler handled the record, and that bubbling is not permitted. 33 | * false means the record was either not processed or that this handler allows bubbling. 34 | */ 35 | public function handle(array $record): bool 36 | { 37 | return $this->doHandle($record); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Debug/Collector/MonologHandler/CompatTrait_3.0.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.5 11 | */ 12 | 13 | namespace bdk\Debug\Collector\MonologHandler; 14 | 15 | use Monolog\LogRecord; 16 | 17 | /* 18 | Wrap in condition. 19 | PHPUnit code coverage scans all files and will conflict 20 | */ 21 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 22 | /** 23 | * Provide handle method (with return type-hint) 24 | */ 25 | trait CompatTrait 26 | { 27 | /** 28 | * Handles a record. 29 | * 30 | * @param LogRecord $record The record to handle 31 | * 32 | * @return bool true means that this handler handled the record, and that bubbling is not permitted. 33 | * false means the record was either not processed or that this handler allows bubbling. 34 | */ 35 | public function handle(LogRecord $record): bool 36 | { 37 | return $this->isHandling($record) 38 | ? $this->doHandle($record->toArray()) 39 | : false; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Debug/Collector/MySqli/ExecuteQueryTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2022-2025 Brad Kent 10 | * @since 3.0 11 | */ 12 | 13 | /* 14 | PHP 8.2's new execute_query method requires signatures to match.. 15 | to maintain compatibility with php < 8.2 we'll use a trait 16 | */ 17 | 18 | namespace bdk\Debug\Collector\MySqli; 19 | 20 | if (PHP_VERSION_ID >= 80200) { 21 | require __DIR__ . '/ExecuteQueryTrait_php8.2.php'; 22 | } else { 23 | /** 24 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 25 | */ 26 | trait ExecuteQueryTrait 27 | { 28 | // execute_query did not exist in PHP < 8.2 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Debug/Collector/MySqli/ExecuteQueryTrait_php8.2.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\MySqli; 14 | 15 | use mysqli_result; 16 | 17 | /** 18 | * Define HP 8.2's mysqli::execute_query method 19 | * 20 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 21 | * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps 22 | */ 23 | trait ExecuteQueryTrait 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function execute_query(string $query, ?array $params = null): mysqli_result|bool // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 29 | { 30 | return $this->profileCall('execute_query', $query, \func_get_args()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Debug/Collector/MySqli/MySqliStmt.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\MySqli; 14 | 15 | use bdk\Debug\Collector\MySqli; 16 | use bdk\Debug\Collector\StatementInfo; 17 | use Exception; 18 | use mysqli_stmt as mysqliStmtBase; 19 | 20 | /** 21 | * A mysqli_stmt proxy which traces statements 22 | */ 23 | class MySqliStmt extends mysqliStmtBase 24 | { 25 | /** @var string */ 26 | private $query; 27 | 28 | /** @var MySqli */ 29 | private $mysqli; 30 | 31 | /** @var list */ 32 | private $params = array(); 33 | 34 | /** @var list */ 35 | private $types = array(); 36 | 37 | /** 38 | * Constructor 39 | * 40 | * @param MySqli $mysqli mysqli instance 41 | * @param string $query SQL query 42 | */ 43 | public function __construct(MySqli $mysqli, $query = null) 44 | { 45 | parent::__construct($mysqli, $query); 46 | $this->mysqli = $mysqli; 47 | $this->query = $query; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | * 53 | * Requires php >= 5.6 (variadic syntax) 54 | * 55 | * @param string $types A string that contains one or more characters which specify the types for the corresponding bind variables 56 | * @param mixed ...$vals The number of variables and length of string types must match the parameters in the statement 57 | * 58 | * @return bool 59 | */ 60 | #[\ReturnTypeWillChange] 61 | public function bind_param($types, &...$vals) // @phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps 62 | { 63 | if ($this->mysqli->connectionAttempted === false) { 64 | return false; 65 | } 66 | $this->params = $vals; 67 | $this->types = \str_split($types); 68 | return parent::bind_param($types, ...$vals); 69 | } 70 | 71 | /** 72 | * {@inheritDoc} 73 | */ 74 | #[\ReturnTypeWillChange] 75 | public function execute($params = null) 76 | { 77 | $statementInfo = new StatementInfo($this->query, $this->params, $this->types); 78 | $return = $this->mysqli->connectionAttempted 79 | ? (PHP_VERSION_ID >= 80100 ? parent::execute($params) : parent::execute()) 80 | : false; 81 | $exception = $this->mysqli->connectionAttempted 82 | ? null 83 | : new Exception('Not connected'); 84 | $statementInfo->end($exception, $return ? $this->affected_rows : null); 85 | $this->mysqli->addStatementInfo($statementInfo); 86 | return $return; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Debug/Collector/Pdo/CompatTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | /* 14 | PHP 8 requires signatures to match.. which relies on variadic.... 15 | to maintain compatibility with ancient PHP we'll do this via trait 16 | */ 17 | 18 | namespace bdk\Debug\Collector\Pdo; 19 | 20 | if (PHP_VERSION_ID >= 50600) { 21 | require __DIR__ . '/CompatTrait_php5.6.php'; 22 | } else { 23 | /** 24 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 25 | */ 26 | trait CompatTrait 27 | { 28 | /** 29 | * Executes an SQL statement, returning a result set as a PDOStatement object 30 | * 31 | * @param string $statement The SQL statement to prepare and execute. 32 | * 33 | * @return \PDOStatement|false PDO::query returns a PDOStatement object, or `false` on failure. 34 | * @link http://php.net/manual/en/pdo.query.php 35 | */ 36 | public function query($statement = null) 37 | { 38 | return $this->profileCall('query', $statement, \func_get_args()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Debug/Collector/Pdo/CompatTrait_php5.6.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\Pdo; 14 | 15 | /** 16 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 17 | */ 18 | trait CompatTrait 19 | { 20 | /** 21 | * Executes an SQL statement, returning a result set as a PDOStatement object 22 | * 23 | * @param string $statement The SQL statement to prepare and execute. 24 | * @param int $fetchMode PDO::FETCH_COLUMN | PDO::FETCH_CLASS | PDO::FETCH_INTO 25 | * @param mixed ...$fetchModeArgs Additional mode dependent args 26 | * 27 | * @return \PDOStatement|false PDO::query returns a PDOStatement object, or `false` on failure. 28 | * @link http://php.net/manual/en/pdo.query.php 29 | */ 30 | #[\ReturnTypeWillChange] 31 | public function query($statement = null, $fetchMode = null, ...$fetchModeArgs) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 32 | { 33 | return $this->profileCall('query', $statement, \func_get_args()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Debug/Collector/SimpleCache/CallInfo.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\SimpleCache; 14 | 15 | use bdk\Debug\AbstractComponent; 16 | use bdk\Debug\Utility; 17 | use Exception; 18 | 19 | /** 20 | * Holds information about a SimpleCache call 21 | * 22 | * @property-read int $duration 23 | * @property-read Exception $exception 24 | * @property-read bool $isSuccess 25 | * @property-read array $keyOrKeys 26 | * @property-read int $memoryEnd 27 | * @property-read int $memoryStart 28 | * @property-read int $memoryUsage 29 | * @property-read string $method 30 | * @property-read float $timeEnd 31 | * @property-read float $timeStart 32 | */ 33 | class CallInfo extends AbstractComponent 34 | { 35 | /** @var int|null */ 36 | protected $duration; 37 | /** @var Exception|null */ 38 | protected $exception; 39 | /** @var array|null */ 40 | protected $keyOrKeys; 41 | /** @var int|null */ 42 | protected $memoryEnd; 43 | /** @var int */ 44 | protected $memoryStart; 45 | /** @var int|null */ 46 | protected $memoryUsage; 47 | /** @var string */ 48 | protected $method = ''; 49 | /** @var float|null */ 50 | protected $timeEnd; 51 | /** @var float */ 52 | protected $timeStart; 53 | 54 | /** @var list */ 55 | protected $readOnly = [ 56 | 'duration', 57 | 'exception', 58 | 'keyOrKeys', 59 | 'memoryEnd', 60 | 'memoryStart', 61 | 'memoryUsage', 62 | 'method', 63 | 'timeEnd', 64 | 'timeStart', 65 | ]; 66 | 67 | /** 68 | * @param string $method method called 69 | * @param mixed $keyOrKeys affected key or keys 70 | */ 71 | public function __construct($method, $keyOrKeys = null) 72 | { 73 | $this->method = $method; 74 | $this->keyOrKeys = $keyOrKeys; 75 | $this->timeStart = \microtime(true); 76 | $this->memoryStart = \memory_get_usage(false); 77 | } 78 | 79 | /** 80 | * Magic method 81 | * 82 | * @return array 83 | */ 84 | public function __debugInfo() 85 | { 86 | return array( 87 | 'duration' => $this->duration, 88 | 'exception' => $this->exception, 89 | 'keyOrKeys' => $this->keyOrKeys, 90 | 'memoryUsage' => $this->memoryUsage, 91 | 'method' => $this->method, 92 | ); 93 | } 94 | 95 | /** 96 | * @param Exception|null $exception Exception (if statement threw exception) 97 | * 98 | * @return void 99 | */ 100 | public function end($exception = null) 101 | { 102 | Utility::assertType($exception, 'Exception'); 103 | 104 | $this->exception = $exception; 105 | $this->timeEnd = \microtime(true); 106 | $this->memoryEnd = \memory_get_usage(false); 107 | $this->duration = $this->timeEnd - $this->timeStart; 108 | $this->memoryUsage = $this->memoryEnd - $this->memoryStart; 109 | } 110 | 111 | /** 112 | * Checks if the statement was successful 113 | * 114 | * @return bool 115 | */ 116 | protected function isSuccess() 117 | { 118 | return $this->exception === null; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Debug/Collector/SimpleCache/CompatTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\SimpleCache; 14 | 15 | $refClass = new \ReflectionClass('Psr\SimpleCache\CacheInterface'); 16 | $refMethod = $refClass->getMethod('get'); 17 | $refParameters = $refMethod->getParameters(); 18 | 19 | if (\method_exists($refMethod, 'hasReturnType') && $refMethod->hasReturnType()) { 20 | // psr/simple-cache 3.0 21 | require __DIR__ . '/CompatTrait_3.php'; 22 | } elseif (\method_exists($refParameters[0], 'hasType') && $refParameters[0]->hasType()) { 23 | // psr/simple-cache 2.0 24 | require __DIR__ . '/CompatTrait_2.php'; 25 | } elseif (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 26 | // psr/simple-cache 1.0 27 | require __DIR__ . '/CompatTrait_1.php'; 28 | } 29 | -------------------------------------------------------------------------------- /src/Debug/Collector/SimpleCache/CompatTrait_1.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\SimpleCache; 14 | 15 | /* 16 | Wrap in condition. 17 | PHPUnit code coverage scans all files and will conflict 18 | */ 19 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 20 | /** 21 | * Provide method signatures compatible with psr/simple-cache 1.x 22 | */ 23 | trait CompatTrait 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function get($key, $default = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 29 | { 30 | return $this->profileCall('get', \func_get_args(), false, $key); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function set($key, $value, $ttl = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 37 | { 38 | return $this->profileCall('set', \func_get_args(), true, $key); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function delete($key) 45 | { 46 | return $this->profileCall('delete', \func_get_args(), false, $key); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function clear() 53 | { 54 | return $this->profileCall('clear', [], true); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function getMultiple($keys, $default = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 61 | { 62 | $keysDebug = $this->keysDebug($keys); 63 | return $this->profileCall('getMultiple', \func_get_args(), false, $keysDebug); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function setMultiple($values, $ttl = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 70 | { 71 | $keysDebug = $this->keysDebug($values, true); 72 | return $this->profileCall('setMultiple', \func_get_args(), true, $keysDebug); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function deleteMultiple($keys) 79 | { 80 | $keysDebug = $this->keysDebug($keys); 81 | return $this->profileCall('deleteMultiple', \func_get_args(), true, $keysDebug); 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | public function has($key) 88 | { 89 | return $this->profileCall('has', \func_get_args(), false, $key); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Debug/Collector/SimpleCache/CompatTrait_2.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\SimpleCache; 14 | 15 | /* 16 | Wrap in condition. 17 | PHPUnit code coverage scans all files and will conflict 18 | */ 19 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 20 | /** 21 | * Provide method signatures compatible with psr/simple-cache 2.x 22 | */ 23 | trait CompatTrait 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function get(string $key, mixed $default = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 29 | { 30 | return $this->profileCall('get', \func_get_args(), false, $key); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 37 | { 38 | return $this->profileCall('set', \func_get_args(), true, $key); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function delete(string $key) 45 | { 46 | return $this->profileCall('delete', \func_get_args(), false, $key); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function clear() 53 | { 54 | return $this->profileCall('clear', [], true); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function getMultiple(string $keys, mixed $default = null) // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 61 | { 62 | $keysDebug = $this->keysDebug($keys); 63 | return $this->profileCall('getMultiple', \func_get_args(), false, $keysDebug); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null) 70 | { 71 | $keysDebug = $this->keysDebug($values, true); 72 | return $this->profileCall('setMultiple', \func_get_args(), true, $keysDebug); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function deleteMultiple(iterable $keys) 79 | { 80 | $keysDebug = $this->keysDebug($keys); 81 | return $this->profileCall('deleteMultiple', \func_get_args(), true, $keysDebug); 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | public function has(string $key) 88 | { 89 | return $this->profileCall('has', \func_get_args(), false, $key); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Debug/Collector/SimpleCache/CompatTrait_3.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Collector\SimpleCache; 14 | 15 | /* 16 | Wrap in condition. 17 | PHPUnit code coverage scans all files and will conflict 18 | */ 19 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 20 | /** 21 | * Provide method signatures compatible with psr/simple-cache 3.x 22 | */ 23 | trait CompatTrait 24 | { 25 | /** 26 | * {@inheritDoc} 27 | */ 28 | public function get(string $key, mixed $default = null): mixed // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 29 | { 30 | return $this->profileCall('get', \func_get_args(), false, $key); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 37 | { 38 | return $this->profileCall('set', \func_get_args(), true, $key); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function delete(string $key): bool 45 | { 46 | return $this->profileCall('delete', \func_get_args(), false, $key); 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | public function clear(): bool 53 | { 54 | return $this->profileCall('clear', [], true); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | public function getMultiple(iterable $keys, mixed $default = null): iterable // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 61 | { 62 | $keysDebug = $this->keysDebug($keys); 63 | return $this->profileCall('getMultiple', \func_get_args(), false, $keysDebug); 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 70 | { 71 | $keysDebug = $this->keysDebug($values, true); 72 | return $this->profileCall('setMultiple', \func_get_args(), true, $keysDebug); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function deleteMultiple(iterable $keys): bool 79 | { 80 | $keysDebug = $this->keysDebug($keys); 81 | return $this->profileCall('deleteMultiple', \func_get_args(), true, $keysDebug); 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | public function has(string $key): bool 88 | { 89 | return $this->profileCall('has', \func_get_args(), false, $key); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Debug/Collector/TwigExtension.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Collector; 14 | 15 | use bdk\Debug; 16 | use Twig\Extension\ProfilerExtension; 17 | use Twig\Profiler\Profile; 18 | 19 | /** 20 | * Profile / log Twig Template 21 | */ 22 | class TwigExtension extends ProfilerExtension 23 | { 24 | /** @var string */ 25 | protected $icon = ':template:'; 26 | 27 | /** @var Debug */ 28 | private $debug; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param Debug|null $debug (optional) Debug instance 34 | * @param Profile|null $profile (optional) Profile instance 35 | * 36 | * @SuppressWarnings(PHPMD.StaticAccess) 37 | */ 38 | public function __construct($debug = null, $profile = null) 39 | { 40 | \bdk\Debug\Utility::assertType($debug, 'bdk\Debug'); 41 | \bdk\Debug\Utility::assertType($profile, 'Twig\Profiler\Profile'); 42 | 43 | if (!$debug) { 44 | $debug = Debug::getChannel('Twig', array('channelIcon' => $this->icon)); 45 | } elseif ($debug === $debug->rootInstance) { 46 | $debug = $debug->getChannel('Twig', array('channelIcon' => $this->icon)); 47 | } 48 | if (!$profile) { 49 | $profile = new Profile(); 50 | } 51 | $this->debug = $debug; 52 | parent::__construct($profile); 53 | } 54 | 55 | /** 56 | * Used by ProfilerNodeVisitor / Profiler\Node\EnterProfileNode 57 | * 58 | * @param Profile $profile Profile instance 59 | * 60 | * @return void 61 | */ 62 | public function enter(Profile $profile) 63 | { 64 | parent::enter($profile); 65 | $this->debug->groupCollapsed( 66 | 'Twig: ' . $profile->getType(), 67 | $profile->getName(), 68 | $this->debug->meta('ungroup') 69 | ); 70 | } 71 | 72 | /** 73 | * Used by ProfilerNodeVisitor / Profiler\Node\LeaveProfileNode 74 | * 75 | * @param Profile $profile Profile instance 76 | * 77 | * @return void 78 | */ 79 | public function leave(Profile $profile) 80 | { 81 | parent::leave($profile); 82 | $haveChildren = \count($profile->getProfiles()) > 0; 83 | $msg = $haveChildren 84 | ? 'Twig: end ' . $profile->getType() . ': ' . $profile->getName() 85 | : 'Twig: ' . $profile->getType() . ': ' . $profile->getName(); 86 | $this->debug->time($msg, $profile->getDuration()); 87 | $this->debug->groupEnd(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Debug/ConfigurableInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug; 14 | 15 | /** 16 | * Interface providing means to get and set configuration 17 | */ 18 | interface ConfigurableInterface 19 | { 20 | /** 21 | * Get config value(s) 22 | * 23 | * @param string $key (optional) key 24 | * 25 | * @return mixed 26 | */ 27 | public function getCfg($key = null); 28 | 29 | /** 30 | * Set one or more config values 31 | * 32 | * setCfg('key', 'value') 33 | * setCfg(array('k1'=>'v1', 'k2'=>'v2')) 34 | * 35 | * @param string $mixed key=>value array or key 36 | * @param mixed $val new value 37 | * 38 | * @return mixed returns previous value(s) 39 | */ 40 | public function setCfg($mixed, $val = null); 41 | } 42 | -------------------------------------------------------------------------------- /src/Debug/Dump/Base/BaseObject.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Dump\Base; 14 | 15 | use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; 16 | use bdk\Debug\Dump\Base\Value as ValDumper; 17 | 18 | /** 19 | * Output object 20 | */ 21 | class BaseObject 22 | { 23 | /** @var ValDumper */ 24 | public $valDumper; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param ValDumper $valDumper Dump\Html instance 30 | */ 31 | public function __construct(ValDumper $valDumper) 32 | { 33 | $this->valDumper = $valDumper; 34 | } 35 | 36 | /** 37 | * Dump object 38 | * 39 | * @param ObjectAbstraction $abs Object Abstraction instance 40 | * 41 | * @return string html fragment 42 | */ 43 | public function dump(ObjectAbstraction $abs) 44 | { 45 | if ($abs['isRecursion']) { 46 | return '(object) ' . $abs['className'] . ' *RECURSION*'; 47 | } 48 | if ($abs['isMaxDepth']) { 49 | return '(object) ' . $abs['className'] . ' *MAX DEPTH*'; 50 | } 51 | if ($abs['isExcluded']) { 52 | return '(object) ' . $abs['className'] . ' NOT INSPECTED'; 53 | } 54 | return array( 55 | '___class_name' => $abs['className'], 56 | ) + (array) $this->dumpObjectProperties($abs); 57 | } 58 | 59 | /** 60 | * Return array of object properties (name->value) 61 | * 62 | * @param ObjectAbstraction $abs Object Abstraction instance 63 | * 64 | * @return array|string 65 | */ 66 | protected function dumpObjectProperties(ObjectAbstraction $abs) 67 | { 68 | $return = array(); 69 | $properties = $abs->sort($abs['properties'], $abs['sort']); 70 | foreach ($properties as $name => $info) { 71 | $name = \str_replace('debug.', '', $name); 72 | $name = $this->valDumper->dump($name, array('addQuotes' => false)); 73 | $info['isInherited'] = $info['declaredLast'] && $info['declaredLast'] !== $abs['className']; 74 | $vis = $this->dumpPropVis($info); 75 | $name = '(' . $vis . ') ' . $name; 76 | $return[$name] = $this->valDumper->dump($info['value']); 77 | } 78 | return $return; 79 | } 80 | 81 | /** 82 | * Dump property visibility 83 | * 84 | * @param array $info Property info 85 | * 86 | * @return string visibility 87 | */ 88 | protected function dumpPropVis(array $info) 89 | { 90 | $vis = (array) $info['visibility']; 91 | foreach ($vis as $i => $v) { 92 | if (\in_array($v, ['magic', 'magic-read', 'magic-write'], true)) { 93 | $vis[$i] = '✨ ' . $v; // "sparkles": there is no magic-wand unicode char 94 | } elseif ($v === 'private' && $info['isInherited']) { 95 | $vis[$i] = '🔒 ' . $v; 96 | } 97 | } 98 | if ($info['debugInfoExcluded']) { 99 | $vis[] = 'excluded'; 100 | } 101 | return \implode(' ', $vis); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Debug/Dump/Html/Object/Cases.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.5 11 | */ 12 | 13 | namespace bdk\Debug\Dump\Html\Object; 14 | 15 | use bdk\Debug\Abstraction\AbstractObject; 16 | use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; 17 | 18 | /** 19 | * Dump object constants and Enum cases as HTML 20 | */ 21 | class Cases extends AbstractSection 22 | { 23 | /** 24 | * Dump enum cases 25 | * 26 | * @param ObjectAbstraction $abs Object Abstraction instance 27 | * 28 | * @return string html fragment 29 | */ 30 | public function dump(ObjectAbstraction $abs) 31 | { 32 | if (\strpos(\json_encode($abs['implements']), '"UnitEnum"') === false) { 33 | return ''; 34 | } 35 | $cfg = array( 36 | 'attributeOutput' => $abs['cfgFlags'] & AbstractObject::CASE_ATTRIBUTE_OUTPUT, 37 | 'collect' => $abs['cfgFlags'] & AbstractObject::CASE_COLLECT, 38 | 'output' => $abs['cfgFlags'] & AbstractObject::CASE_OUTPUT, 39 | ); 40 | if (!$cfg['output']) { 41 | return ''; 42 | } 43 | if (!$cfg['collect']) { 44 | return '
cases not collected
' . "\n"; 45 | } 46 | if (!$abs['cases']) { 47 | return '
no cases!
' . "\n"; 48 | } 49 | return '
cases
' . "\n" 50 | . $this->dumpItems($abs, 'cases', $cfg); 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | protected function getClasses(array $info) 57 | { 58 | return ['case']; 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | protected function getModifiers(array $info) 65 | { 66 | return []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Debug/Dump/Html/Object/Constants.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b2 11 | */ 12 | 13 | namespace bdk\Debug\Dump\Html\Object; 14 | 15 | use bdk\Debug\Abstraction\AbstractObject; 16 | use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; 17 | 18 | /** 19 | * Dump object constants as HTML 20 | */ 21 | class Constants extends AbstractSection 22 | { 23 | /** 24 | * Dump object constants 25 | * 26 | * @param ObjectAbstraction $abs Object Abstraction instance 27 | * 28 | * @return string html fragment 29 | */ 30 | public function dump(ObjectAbstraction $abs) 31 | { 32 | $cfg = array( 33 | 'attributeOutput' => $abs['cfgFlags'] & AbstractObject::CONST_ATTRIBUTE_OUTPUT, 34 | 'collect' => $abs['cfgFlags'] & AbstractObject::CONST_COLLECT, 35 | 'output' => $abs['cfgFlags'] & AbstractObject::CONST_OUTPUT, 36 | ); 37 | if (!$cfg['output']) { 38 | return ''; 39 | } 40 | if (!$cfg['collect']) { 41 | return '
constants not collected
' . "\n"; 42 | } 43 | if (!$abs['constants']) { 44 | return ''; 45 | } 46 | $html = '
constants
' . "\n"; 47 | $html .= $this->dumpItems($abs, 'constants', $cfg); 48 | return $html; 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | protected function getClasses(array $info) 55 | { 56 | $visClasses = \array_diff((array) $info['visibility'], ['debug']); 57 | $classes = \array_keys(\array_filter(array( 58 | 'constant' => true, 59 | 'isFinal' => $info['isFinal'], 60 | 'private-ancestor' => $info['isPrivateAncestor'], 61 | ))); 62 | return \array_merge($classes, $visClasses); 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | protected function getModifiers(array $info) 69 | { 70 | return \array_merge(\array_keys(\array_filter(array( 71 | 'final' => $info['isFinal'], 72 | ))), (array) $info['visibility']); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Debug/Dump/Html/Object/Properties.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Dump\Html\Object; 14 | 15 | use bdk\Debug\Abstraction\AbstractObject; 16 | use bdk\Debug\Abstraction\Object\Abstraction as ObjectAbstraction; 17 | 18 | /** 19 | * Dump object properties as HTML 20 | */ 21 | class Properties extends AbstractSection 22 | { 23 | /** 24 | * Dump object properties as HTML 25 | * 26 | * @param ObjectAbstraction $abs Object Abstraction instance 27 | * 28 | * @return string html fragment 29 | */ 30 | public function dump(ObjectAbstraction $abs) 31 | { 32 | $cfg = array( 33 | 'attributeOutput' => $abs['cfgFlags'] & AbstractObject::PROP_ATTRIBUTE_OUTPUT, 34 | ); 35 | if ($abs['isInterface']) { 36 | return ''; 37 | } 38 | $magicMethods = \array_intersect(['__get', '__set'], \array_keys($abs['methods'])); 39 | $html = '
' . $this->getLabel($abs) . '
' . "\n"; 40 | $html .= $this->magicMethodInfo($magicMethods); 41 | $html .= $this->dumpItems($abs, 'properties', $cfg); 42 | return $html; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | protected function getClasses(array $info) 49 | { 50 | $visClasses = \array_diff((array) $info['visibility'], ['debug']); 51 | $classes = \array_keys(\array_filter(array( 52 | 'debug-value' => $info['valueFrom'] === 'debug', 53 | 'debuginfo-excluded' => $info['debugInfoExcluded'], 54 | 'debuginfo-value' => $info['valueFrom'] === 'debugInfo', 55 | 'forceShow' => $info['forceShow'], 56 | 'getHook' => \in_array('get', $info['hooks'], true), 57 | 'isDeprecated' => $info['isDeprecated'], 58 | 'isDynamic' => $info['declaredLast'] === null 59 | && $info['valueFrom'] === 'value' 60 | && $info['objClassName'] !== 'stdClass', 61 | 'isEager' => !empty($info['isEager']), 62 | 'isFinal' => $info['isFinal'], 63 | 'isPromoted' => $info['isPromoted'], 64 | 'isReadOnly' => $info['isReadOnly'], 65 | 'isStatic' => $info['isStatic'], 66 | 'isVirtual' => $info['isVirtual'], 67 | 'isWriteOnly' => $info['isVirtual'] && \in_array('get', $info['hooks'], true) === false, 68 | 'private-ancestor' => $info['isPrivateAncestor'], 69 | 'property' => true, 70 | 'setHook' => \in_array('set', $info['hooks'], true), 71 | ))); 72 | return \array_merge($classes, $visClasses); 73 | } 74 | 75 | /** 76 | * get property "header" 77 | * 78 | * @param ObjectAbstraction $abs Object Abstraction instance 79 | * 80 | * @return string html fragment 81 | */ 82 | protected function getLabel(ObjectAbstraction $abs) 83 | { 84 | if (\count($abs['properties']) === 0) { 85 | return 'no properties'; 86 | } 87 | $label = 'properties'; 88 | if ($abs['viaDebugInfo']) { 89 | $label .= ' (via __debugInfo)'; 90 | } 91 | return $label; 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | protected function getModifiers(array $info) 98 | { 99 | $info = \array_merge(array( 100 | 'isEager' => null, // only collected on isLazy objects 101 | ), $info); 102 | $modifiers = \array_merge( 103 | array( 104 | 'eager' => $info['isEager'], 105 | 'final' => $info['isFinal'], 106 | ), 107 | \array_fill_keys((array) $info['visibility'], true), 108 | array( 109 | 'readonly' => $info['isReadOnly'], 110 | 'static' => $info['isStatic'], 111 | ) 112 | ); 113 | return \array_keys(\array_filter($modifiers)); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Debug/Framework/Laravel/CacheEventsSubscriber.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Framework\Laravel; 14 | 15 | use bdk\Debug; 16 | use Illuminate\Cache\Events\CacheEvent; 17 | use Illuminate\Cache\Events\CacheHit; 18 | use Illuminate\Cache\Events\CacheMissed; 19 | use Illuminate\Cache\Events\KeyForgotten; 20 | use Illuminate\Cache\Events\KeyWritten; 21 | use Illuminate\Events\Dispatcher; 22 | 23 | /** 24 | * Log cache events 25 | */ 26 | class CacheEventsSubscriber 27 | { 28 | /** @var array */ 29 | protected $classMap = array( 30 | CacheHit::class => 'hit', 31 | CacheMissed::class => 'missed', 32 | KeyForgotten::class => 'forgotten', 33 | KeyWritten::class => 'written', 34 | ); 35 | 36 | /** @var Debug */ 37 | protected $debug; 38 | 39 | /** @var array */ 40 | protected $options = array( 41 | 'collectValues' => true, 42 | 'icon' => ':cache:', 43 | ); 44 | 45 | /** 46 | * Constructor 47 | * 48 | * @param array $options Options 49 | * @param Debug|null $debug (optional) Specify PHPDebugConsole instance 50 | * if not passed, will create PDO channel on singleton instance 51 | * if root channel is specified, will create a PDO channel 52 | * 53 | * @SuppressWarnings(PHPMD.StaticAccess) 54 | */ 55 | public function __construct($options = array(), $debug = null) 56 | { 57 | \bdk\Debug\Utility::assertType($debug, 'bdk\Debug'); 58 | 59 | $this->options = \array_merge($this->options, $options); 60 | $channelOptions = array( 61 | 'channelIcon' => $this->options['icon'], 62 | 'channelShow' => false, 63 | ); 64 | if (!$debug) { 65 | $debug = Debug::getChannel('cache', $channelOptions); 66 | } elseif ($debug === $debug->rootInstance) { 67 | $debug = $debug->getChannel('cache', $channelOptions); 68 | } 69 | $this->debug = $debug; 70 | } 71 | 72 | /** 73 | * Log cache event 74 | * 75 | * @param CacheEvent $event cache event instance 76 | * 77 | * @return void 78 | */ 79 | public function onCacheEvent(CacheEvent $event) 80 | { 81 | $label = $this->classMap[\get_class($event)]; 82 | $params = \get_object_vars($event); 83 | $this->debug->log($label, $params); 84 | } 85 | 86 | /** 87 | * Subscribe to events 88 | * 89 | * @param Dispatcher $dispatcher Dispatcher interface 90 | * 91 | * @return void 92 | */ 93 | public function subscribe(Dispatcher $dispatcher) 94 | { 95 | foreach (\array_keys($this->classMap) as $eventClass) { 96 | $dispatcher->listen($eventClass, [$this, 'onCacheEvent']); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Debug/Framework/Laravel/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Framework\Laravel; 14 | 15 | use bdk\Debug\Framework\Laravel\ServiceProvider; 16 | use Illuminate\Foundation\Support\Providers\EventServiceProvider as BaseEventServiceProvider; 17 | use Symfony\Component\HttpKernel\KernelEvents; 18 | 19 | /** 20 | * EventServiceProvider 21 | */ 22 | class EventServiceProvider extends BaseEventServiceProvider 23 | { 24 | protected $listen = [ 25 | KernelEvents::REQUEST => [ 26 | [ServiceProvider::class, 'onRequest'], 27 | ], 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /src/Debug/Framework/Laravel/config.php: -------------------------------------------------------------------------------- 1 | Env::get('PHPDEBUGCONSOLE_KEY', \substr(\md5(\uniqid(\rand(), true)), 0, 10)), 8 | 9 | /* 10 | Laravel specific 11 | */ 12 | 'laravel' => [ 13 | 'auth' => true, // Display Laravel authentication status 14 | 'cacheEvents' => true, // Display cache events 15 | 'config' => true, // Display config settings 16 | 'db' => true, // Show database (PDO) queries and bindings 17 | 'events' => true, // All events fired 18 | 'gate' => true, // Display Laravel Gate checks 19 | 'laravel' => true, // Laravel version and environment 20 | 'mail' => true, // Catch mail messages 21 | 'models' => true, // Display models 22 | 'route' => true, // Current route information 23 | 'session' => true, // Display initial session data 24 | 'views' => true, // Views with their data 25 | ], 26 | 27 | /* 28 | Laravel specific options 29 | */ 30 | 'options' => [ 31 | 'cacheEvents' => [ 32 | 'values' => true, // collect cache values 33 | ], 34 | 'route' => [ 35 | 'label' => true, // show complete route 36 | ], 37 | 'views' => [ 38 | 'data' => 'type', // bool|'type' 39 | ], 40 | ], 41 | ); 42 | -------------------------------------------------------------------------------- /src/Debug/Framework/Slim2.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug\Framework; 14 | 15 | use bdk\Debug; 16 | use Slim\Log; 17 | 18 | /** 19 | * A Slim v2 log writer 20 | * 21 | * `$app->log->setWriter(new \bdk\Debug\Framework\Slim2($debug));` 22 | */ 23 | class Slim2 24 | { 25 | /** @var Debug */ 26 | private $debug; 27 | 28 | /** @var object */ 29 | private $prevWriter; 30 | 31 | /** 32 | * Constructor 33 | * 34 | * @param Debug|null $debug (optional) Specify PHPDebugConsole instance 35 | * if not passed, will create Slim channel on singleton instance 36 | * if root channel is specified, will create a Slim channel 37 | * @param object $prevWriter (optional) previous slim logWriter if desired to continue writing to existing writer 38 | * 39 | * @SuppressWarnings(PHPMD.StaticAccess) 40 | */ 41 | public function __construct($debug = null, $prevWriter = null) 42 | { 43 | \bdk\Debug\Utility::assertType($debug, 'bdk\Debug'); 44 | \bdk\Debug\Utility::assertType($prevWriter, 'object'); // object not avail as type-hint until php 7.2 45 | 46 | if (!$debug) { 47 | $debug = Debug::getChannel('Slim'); 48 | } elseif ($debug === $debug->rootInstance) { 49 | $debug = $debug->getChannel('Slim'); 50 | } 51 | /* 52 | Determine filepath for slim's logger so we skip over it when determining where warn/errors originate 53 | */ 54 | $debug->backtrace->addInternalClass(\get_class(\Slim\Slim::getInstance()->log)); 55 | $this->debug = $debug; 56 | $this->prevWriter = $prevWriter; 57 | } 58 | 59 | /** 60 | * "Write" a slim log message 61 | * 62 | * @param mixed $message message 63 | * @param int $level slim error level 64 | * 65 | * @return void 66 | */ 67 | public function write($message, $level) 68 | { 69 | if ($this->prevWriter) { 70 | $this->prevWriter->write($message, $level); 71 | } 72 | $method = $this->levelToMethod($level); 73 | $this->debug->{$method}($message); 74 | } 75 | 76 | /** 77 | * Slim Log level to debug message 78 | * 79 | * @param int $level Slim Log level constant 80 | * 81 | * @return string method name 82 | */ 83 | protected function levelToMethod($level) 84 | { 85 | // phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder 86 | $map = array( 87 | Log::EMERGENCY => 'error', 88 | Log::ALERT => 'alert', 89 | Log::CRITICAL => 'error', 90 | Log::ERROR => 'error', 91 | Log::WARN => 'warn', 92 | Log::NOTICE => 'warn', 93 | Log::INFO => 'info', 94 | Log::DEBUG => 'log', 95 | ); 96 | return $map[$level]; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Debug/Framework/Symfony/DebugBundle/BdkDebugBundle.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Framework\Symfony\DebugBundle; 14 | 15 | use Symfony\Component\HttpKernel\Bundle\Bundle; 16 | 17 | /** 18 | * PHPDebugConsole bundle 19 | */ 20 | class BdkDebugBundle extends Bundle 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /src/Debug/Framework/Symfony/DebugBundle/DependencyInjection/BdkDebugExtension.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Framework\Symfony\DebugBundle\DependencyInjection; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Collector\DoctrineMiddleware; 17 | use Symfony\Component\Config\FileLocator; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Extension\Extension; 20 | use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; 21 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 22 | use Symfony\Component\DependencyInjection\Reference; 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | class BdkDebugExtension extends Extension implements PrependExtensionInterface 28 | { 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | public function load(array $configs, ContainerBuilder $container): void 33 | { 34 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 35 | $loader->load('config.yml'); 36 | 37 | $definition = $container->getDefinition(Debug::class); 38 | 39 | /* 40 | Config will get passed to constructor (or factory) defined in config 41 | */ 42 | $definition->setArgument(0, $configs[0]); 43 | } 44 | 45 | /** 46 | * "Prepend" / modify other extension config 47 | * 48 | * @param ContainerBuilder $container ContainerBuilder instance 49 | * 50 | * @return void 51 | */ 52 | public function prepend(ContainerBuilder $container): void 53 | { 54 | // doctrine v3.2 added setMiddlewares 55 | // doctrine v3.3 added AbstractConnectionMiddleware (and other abstract classes) 56 | // doctrine v3.4 deprecated SqlLogger 57 | $doctrineSupportsMiddleware = \method_exists('Doctrine\DBAL\Configuration', 'setMiddlewares') 58 | && \class_exists('Doctrine\DBAL\Driver\Middleware\AbstractConnectionMiddleware'); 59 | if ($doctrineSupportsMiddleware) { 60 | $container->register('doctrineMiddleware', DoctrineMiddleware::class) 61 | ->addTag('doctrine.middleware') 62 | ->addArgument(new Reference('bdk_debug')); 63 | } 64 | 65 | $kernelDebug = $container->getParameter('kernel.debug'); 66 | if ($kernelDebug) { 67 | $container->prependExtensionConfig('framework', array( 68 | 'php_errors' => array( 69 | 'throw' => false, 70 | ), 71 | )); 72 | } 73 | 74 | $container->prependExtensionConfig('monolog', array( 75 | 'handlers' => array( 76 | 'phpDebugConsole' => array( 77 | 'id' => 'monologHandler', 78 | 'type' => 'service', 79 | ), 80 | ), 81 | )); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Debug/Framework/Symfony/DebugBundle/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bkdotcom/PHPDebugConsole/a73802787464abb5af8a4ad5a50f6cf1a40280f0/src/Debug/Framework/Symfony/DebugBundle/LICENSE -------------------------------------------------------------------------------- /src/Debug/Framework/Symfony/DebugBundle/README.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Make sure Composer is installed globally, as explained in the 5 | [installation chapter](https://getcomposer.org/doc/00-intro.md) 6 | of the Composer documentation. 7 | 8 | Applications that use Symfony Flex 9 | ---------------------------------- 10 | 11 | Open a command console, enter your project directory and execute: 12 | 13 | ```console 14 | $ composer require 15 | ``` 16 | 17 | Applications that don't use Symfony Flex 18 | ---------------------------------------- 19 | 20 | ### Step 1: Download the Bundle 21 | 22 | Open a command console, enter your project directory and execute the 23 | following command to download the latest stable version of this bundle: 24 | 25 | ```console 26 | $ composer require 27 | ``` 28 | 29 | ### Step 2: Enable the Bundle 30 | 31 | Then, enable the bundle by adding it to the list of registered bundles 32 | in the `config/bundles.php` file of your project: 33 | 34 | ```php 35 | // config/bundles.php 36 | 37 | return [ 38 | // ... 39 | bdk\Debug\Framework\Symfony\DebugBundle\BdkDebugBundle::class => ['all' => true], 40 | ]; 41 | ``` 42 | -------------------------------------------------------------------------------- /src/Debug/Framework/Symfony/DebugBundle/Resources/config/config.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # KEYS 3 | # 4 | # "abstract" 5 | # "alias" 6 | # "arguments" 7 | # "autoconfigure" 8 | # "autowire" 9 | # "bind" 10 | # "calls" 11 | # "class" 12 | # "configurator" 13 | # "decorates" 14 | # "decoration_inner_name" 15 | # "decoration_on_invalid" 16 | # "decoration_priority" 17 | # "deprecated" 18 | # "factory" 19 | # "file" 20 | # "lazy" 21 | # "parent" 22 | # "properties" 23 | # "public" 24 | # "shared" 25 | # "synthetic" 26 | # "tags" 27 | _defaults: 28 | autowire: true # Automatically injects dependencies in your services. 29 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 30 | 31 | bdk\Debug: 32 | factory: ['bdk\Debug', 'getInstance'] 33 | public: false 34 | bdk_debug: 35 | alias: bdk\Debug 36 | public: true 37 | bdk_debug_listener: 38 | class: bdk\Debug\Framework\Symfony\DebugBundle\EventListener\BdkDebugBundleListener 39 | arguments: ['@bdk_debug', '@kernel', '@doctrine'] 40 | tags: ['kernel.event_subscriber'] 41 | bdk_debug_twig: 42 | class: bdk\Debug\Collector\TwigExtension 43 | arguments: ['@bdk_debug'] 44 | tags: ['twig.extension'] 45 | monologHandler: 46 | class: bdk\Debug\Collector\MonologHandler 47 | arguments: ['@bdk_debug'] 48 | tags: ['monolog.logger'] 49 | # - { name: monolog.logger, channel: country } 50 | -------------------------------------------------------------------------------- /src/Debug/Framework/Yii1_1/PdoCollector.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Framework\Yii1_1; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Collector\Pdo; 17 | use CApplicationComponent; 18 | use CDbConnection; 19 | use ReflectionObject; 20 | use Yii; 21 | 22 | /** 23 | * Collect Pdo info 24 | */ 25 | class PdoCollector 26 | { 27 | /** @var CApplicationComponent */ 28 | protected $component; 29 | 30 | /** @var array */ 31 | protected $pdoInstances = array(); 32 | 33 | /** 34 | * Constructor 35 | * 36 | * @param CApplicationComponent $component Debug component 37 | */ 38 | public function __construct(CApplicationComponent $component) 39 | { 40 | $this->component = $component; 41 | } 42 | 43 | /** 44 | * Setup up PDO collector 45 | * Log to PDO channel 46 | * 47 | * @param CDbConnection|null $dbConnection CDbConnection instance 48 | * 49 | * @return void 50 | */ 51 | public function collect($dbConnection = null) 52 | { 53 | \bdk\Debug\Utility::assertType($dbConnection, 'CDbConnection'); 54 | 55 | if ($this->component->shouldCollect('pdo') === false) { 56 | return; 57 | } 58 | $dbConnection = $dbConnection ?: Yii::app()->db; 59 | $dbConnection->active = true; // creates pdo obj 60 | $pdo = $dbConnection->pdoInstance; 61 | if ($pdo instanceof Pdo) { 62 | // already wrapped 63 | return; 64 | } 65 | $connectionString = $dbConnection->connectionString; 66 | $pdoChannel = $this->getChannel($dbConnection); 67 | $pdoCollector = new Pdo($pdo, $pdoChannel); 68 | $this->updateDbConnection($dbConnection, $pdoCollector); 69 | $this->pdoInstances[$connectionString] = $pdoCollector; 70 | } 71 | 72 | /** 73 | * Get PDO instance for given connection string 74 | * 75 | * @param string $connectionString connection string 76 | * 77 | * @return Pdo 78 | */ 79 | public function getInstance($connectionString) 80 | { 81 | return $this->pdoInstances[$connectionString]; 82 | } 83 | 84 | /** 85 | * Get PDO Debug Channel for given db connection 86 | * 87 | * @param CDbConnection $dbConnection CDbConnection instance 88 | * 89 | * @return Debug 90 | */ 91 | private function getChannel(CDbConnection $dbConnection) 92 | { 93 | $channelName = 'PDO'; 94 | if (\strpos($dbConnection->connectionString, 'master=true')) { 95 | $channelName .= ' (master)'; 96 | } elseif (\strpos($dbConnection->connectionString, 'slave=true')) { 97 | $channelName .= ' (slave)'; 98 | } 99 | // nest the PDO channel under our Yii channel 100 | return $this->component->debug->getChannel($channelName, array( 101 | 'channelIcon' => ':database:', 102 | 'channelShow' => false, 103 | )); 104 | } 105 | 106 | /** 107 | * Attach PDO Collector to dbConnection 108 | * 109 | * @param CDbConnection $dbConnection CDbConnection instance 110 | * @param Pdo $pdoCollector PDO collector instance 111 | * 112 | * @return void 113 | */ 114 | private function updateDbConnection(CDbConnection $dbConnection, Pdo $pdoCollector) 115 | { 116 | $dbRefObj = new ReflectionObject($dbConnection); 117 | while (!$dbRefObj->hasProperty('_pdo')) { 118 | $dbRefObj = $dbRefObj->getParentClass(); 119 | if ($dbRefObj === false) { 120 | $this->component->debug->warn('unable to initiate PDO collector'); 121 | } 122 | } 123 | $pdoPropObj = $dbRefObj->getProperty('_pdo'); 124 | $pdoPropObj->setAccessible(true); 125 | $pdoPropObj->setValue($dbConnection, $pdoCollector); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Debug/Framework/Yii1_1/UserInfo.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Framework\Yii1_1; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Abstraction\Type; 17 | use CApplicationComponent; 18 | use CModel; 19 | use CWebApplication; 20 | use Exception; 21 | use Yii; 22 | 23 | /** 24 | * Collect Pdo info 25 | * 26 | * @SuppressWarnings(PHPMD.StaticAccess) 27 | */ 28 | class UserInfo 29 | { 30 | protected $component; 31 | 32 | /** 33 | * Constructor 34 | * 35 | * @param CApplicationComponent $component Debug component 36 | */ 37 | public function __construct(CApplicationComponent $component) 38 | { 39 | $this->component = $component; 40 | } 41 | 42 | /** 43 | * Log current user info 44 | * 45 | * @return void 46 | */ 47 | public function log() 48 | { 49 | if ($this->component->shouldCollect('user') === false) { 50 | return; 51 | } 52 | 53 | $user = Yii::app()->user; 54 | if (\method_exists($user, 'getIsGuest') && $user->getIsGuest()) { 55 | return; 56 | } 57 | 58 | $debug = $this->component->debug->rootInstance->getChannel('User', array( 59 | 'channelIcon' => ':user:', 60 | 'nested' => false, 61 | )); 62 | 63 | $this->logIdentityData($user, $debug); 64 | $this->logAuthClass($debug); 65 | } 66 | 67 | /** 68 | * Log user attributes 69 | * 70 | * @param CApplicationComponent $user User instance (web or console) 71 | * @param Debug $debug Debug instance 72 | * 73 | * @return void 74 | */ 75 | private function logIdentityData(CApplicationComponent $user, Debug $debug) 76 | { 77 | $identityData = $user->model->attributes; 78 | if ($user->model instanceof CModel) { 79 | $identityData = array(); 80 | foreach ($user->model->attributes as $key => $val) { 81 | $key = $user->model->getAttributeLabel($key); 82 | $identityData[$key] = $val; 83 | } 84 | } 85 | $debug->table(\get_class($user), $identityData); 86 | } 87 | 88 | /** 89 | * Log auth & access manager info 90 | * 91 | * @param Debug $debug Debug instance 92 | * 93 | * @return void 94 | */ 95 | private function logAuthClass(Debug $debug) 96 | { 97 | try { 98 | $yiiApp = Yii::app(); 99 | 100 | if (!($yiiApp instanceof CWebApplication)) { 101 | return; 102 | } 103 | 104 | $typeIdentifierClassnameVals = array( 105 | 'type' => Type::TYPE_IDENTIFIER, 106 | 'typeMore' => Type::TYPE_IDENTIFIER_CLASSNAME, 107 | ); 108 | 109 | $authManager = $yiiApp->getAuthManager(); 110 | $debug->log('authManager class', $debug->abstracter->crateWithVals( 111 | \get_class($authManager), 112 | $typeIdentifierClassnameVals 113 | )); 114 | 115 | $accessManager = $yiiApp->getComponent('accessManager'); 116 | if ($accessManager) { 117 | $debug->log('accessManager class', $debug->abstracter->crateWithVals( 118 | \get_class($accessManager), 119 | $typeIdentifierClassnameVals 120 | )); 121 | } 122 | } catch (Exception $e) { 123 | $debug->error('Exception logging user info'); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Debug/Plugin/CustomMethodTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0b1 11 | */ 12 | 13 | namespace bdk\Debug\Plugin; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\LogEntry; 17 | 18 | /** 19 | * Add request/response related methods to debug 20 | */ 21 | trait CustomMethodTrait 22 | { 23 | /** @var Debug */ 24 | private $debug; 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getSubscriptions() 30 | { 31 | return array( 32 | Debug::EVENT_CUSTOM_METHOD => 'onCustomMethod', 33 | ); 34 | } 35 | 36 | /** 37 | * Debug::EVENT_CUSTOM_METHOD event subscriber 38 | * 39 | * @param LogEntry $logEntry logEntry instance 40 | * 41 | * @return void 42 | */ 43 | public function onCustomMethod(LogEntry $logEntry) 44 | { 45 | $method = $logEntry['method']; 46 | if (\in_array($method, $this->methods, true) === false) { 47 | return; 48 | } 49 | $this->debug = $logEntry->getSubject(); 50 | $args = $logEntry['args']; 51 | $meta = $logEntry['meta']; 52 | if ($meta) { 53 | $args[] = $this->debug->meta($meta); 54 | } 55 | $logEntry['handled'] = true; 56 | $logEntry['return'] = \call_user_func_array([$this, $method], $args); 57 | $logEntry->stopPropagation(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Debug/Plugin/Method/Output.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.5 11 | */ 12 | 13 | namespace bdk\Debug\Plugin\Method; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\Plugin\CustomMethodTrait; 17 | use bdk\PubSub\Event; 18 | use bdk\PubSub\SubscriberInterface; 19 | 20 | /** 21 | * Output method 22 | */ 23 | class Output implements SubscriberInterface 24 | { 25 | use CustomMethodTrait; 26 | 27 | /** @var string[] */ 28 | protected $methods = [ 29 | 'output', 30 | ]; 31 | 32 | /** 33 | * Constructor 34 | * 35 | * @codeCoverageIgnore 36 | */ 37 | public function __construct() 38 | { 39 | } 40 | 41 | /** 42 | * Return debug log output 43 | * 44 | * Publishes `Debug::EVENT_OUTPUT` event and returns event's 'return' value 45 | * 46 | * If output config value is `false`, null will be returned. 47 | * 48 | * Note: Log output is handled automatically, and calling output is generally not necessary. 49 | * 50 | * @param array $cfg Override any config values 51 | * 52 | * @return string|null 53 | * 54 | * @since 1.2 explicitly calling output() is no longer necessary.. log will be output automatically via shutdown function 55 | * @since 2.3 `$config` parameter 56 | */ 57 | public function output($cfg = array()) 58 | { 59 | $debug = $this->debug; 60 | $cfgRestore = $debug->config->set($cfg); 61 | if ($debug->getCfg('output', Debug::CONFIG_DEBUG) === false) { 62 | $debug->config->set($cfgRestore); 63 | $debug->obEnd(); 64 | return null; 65 | } 66 | $event = $this->publishOutputEvent(); 67 | if (!$debug->parentInstance) { 68 | $debug->data->set('outputSent', true); 69 | } 70 | $debug->config->set($cfgRestore); 71 | $debug->obEnd(); 72 | return $event['return']; 73 | } 74 | 75 | /** 76 | * Publish Debug::EVENT_OUTPUT 77 | * on all descendant channels 78 | * rootInstance 79 | * finally ourself 80 | * This isn't outputting each channel, but for performing any per-channel "before output" activities 81 | * 82 | * @return Event 83 | */ 84 | private function publishOutputEvent() 85 | { 86 | $debug = $this->debug; 87 | $channels = $debug->getChannels(true); 88 | if ($debug !== $debug->rootInstance) { 89 | $channels[] = $debug->rootInstance; 90 | } 91 | $channels[] = $debug; 92 | foreach ($channels as $channel) { 93 | if ($channel->getCfg('output', Debug::CONFIG_DEBUG) === false) { 94 | continue; 95 | } 96 | $event = $channel->eventManager->publish( 97 | Debug::EVENT_OUTPUT, 98 | $channel, 99 | array( 100 | 'headers' => array(), 101 | 'isTarget' => $channel === $debug, 102 | 'return' => '', 103 | ) 104 | ); 105 | } 106 | /** @var Event */ 107 | return $event; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Debug/Plugin/Runtime.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.5 11 | */ 12 | 13 | namespace bdk\Debug\Plugin; 14 | 15 | use bdk\Debug; 16 | use bdk\Debug\AbstractComponent; 17 | use bdk\PubSub\Event; 18 | use bdk\PubSub\Manager as EventManager; 19 | use bdk\PubSub\SubscriberInterface; 20 | 21 | /** 22 | * Record and output runtime values 23 | * - memoryLimit 24 | * - memoryPeakUsage 25 | * - runtime 26 | */ 27 | class Runtime extends AbstractComponent implements SubscriberInterface 28 | { 29 | /** @var Debug|null */ 30 | private $debug; 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | public function getSubscriptions() 36 | { 37 | return array( 38 | Debug::EVENT_OUTPUT => 'onOutput', 39 | Debug::EVENT_PLUGIN_INIT => 'onPluginInit', 40 | EventManager::EVENT_PHP_SHUTDOWN => ['onShutdown', PHP_INT_MAX], 41 | ); 42 | } 43 | 44 | /** 45 | * Log our runtime info in a summary group 46 | * 47 | * As we're only subscribed to root debug instance's Debug::EVENT_OUTPUT event, this info 48 | * will not be output for any sub-channels output directly 49 | * 50 | * @param Event $event Debug::EVENT_OUTPUT event object 51 | * 52 | * @return void 53 | */ 54 | public function onOutput(Event $event) 55 | { 56 | if ($event['isTarget'] === false) { 57 | return; 58 | } 59 | if (!$this->debug->getCfg('logRuntime', Debug::CONFIG_DEBUG)) { 60 | return; 61 | } 62 | $vals = $this->runtimeVals(); 63 | $route = $this->debug->getCfg('route'); 64 | /** @psalm-suppress TypeDoesNotContainType */ 65 | $isRouteHtml = $route && \get_class($route) === 'bdk\\Debug\\Route\\Html'; 66 | $this->debug->groupSummary(1); 67 | $this->debug->info('Built In ' . $this->debug->utility->formatDuration($vals['runtime'])); 68 | $this->debug->info( 69 | 'Peak Memory Usage' 70 | . ($isRouteHtml 71 | ? ' ' 72 | : '') 73 | . ': ' 74 | . $this->debug->utility->getBytes($vals['memoryPeakUsage']) . ' / ' 75 | . ($vals['memoryLimit'] === '-1' 76 | ? '∞' 77 | : $this->debug->utility->getBytes($vals['memoryLimit']) 78 | ), 79 | $this->debug->meta('sanitize', false) 80 | ); 81 | $this->debug->groupEnd(); 82 | } 83 | 84 | /** 85 | * Debug::EVENT_PLUGIN_INIT subscriber 86 | * 87 | * @param Event $event Debug::EVENT_PLUGIN_INIT Event instance 88 | * 89 | * @return void 90 | */ 91 | public function onPluginInit(Event $event) 92 | { 93 | $this->debug = $event->getSubject(); 94 | } 95 | 96 | /** 97 | * Debug::EVENT_OUTPUT SUBSCRIBER 98 | * 99 | * @return void 100 | */ 101 | public function onShutdown() 102 | { 103 | $this->runtimeVals(); 104 | } 105 | 106 | /** 107 | * Get/store values such as runtime & peak memory usage 108 | * 109 | * @return array 110 | */ 111 | private function runtimeVals() 112 | { 113 | $vals = $this->debug->data->get('runtime'); 114 | if (!$vals) { 115 | $vals = array( 116 | 'memoryLimit' => $this->debug->php->memoryLimit(), 117 | 'memoryPeakUsage' => \memory_get_peak_usage(true), 118 | 'runtime' => $this->debug->timeEnd('requestTime', false, true), 119 | ); 120 | $this->debug->data->set('runtime', $vals); 121 | } 122 | return $vals; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Debug/PluginInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.3 11 | */ 12 | 13 | namespace bdk\Debug; 14 | 15 | use bdk\Debug; 16 | 17 | /** 18 | * Plugin Interface 19 | */ 20 | interface PluginInterface 21 | { 22 | /** 23 | * Set Debug instance 24 | * 25 | * @param Debug $debug Debug instance 26 | * 27 | * @return void 28 | */ 29 | public function setDebug(Debug $debug); 30 | } 31 | -------------------------------------------------------------------------------- /src/Debug/Psr3/CompatTrait.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.1 11 | */ 12 | 13 | namespace bdk\Debug\Psr3; 14 | 15 | /* 16 | psr/log's function signature takes many forms 17 | */ 18 | 19 | $refClass = new \ReflectionClass('Psr\Log\LoggerInterface'); 20 | $refMethod = $refClass->getMethod('log'); 21 | $refParameters = $refMethod->getParameters(); 22 | 23 | if (\method_exists($refMethod, 'hasReturnType') && $refMethod->hasReturnType()) { 24 | // psr/log 3.0 25 | require __DIR__ . '/CompatTrait_3.php'; 26 | } elseif (\method_exists($refParameters[1], 'hasType') && $refParameters[1]->hasType()) { 27 | // psr/log 2.0 28 | require __DIR__ . '/CompatTrait_2.php'; 29 | } elseif (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 30 | /** 31 | * psr/log 1.0 32 | * 33 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 34 | */ 35 | trait CompatTrait 36 | { 37 | /** 38 | * Logs with an arbitrary level. 39 | * 40 | * @param mixed $level debug, info, notice, warning, error, critical, alert, emergency 41 | * @param string|\Stringable $message message 42 | * @param mixed[] $context array 43 | * 44 | * @return void 45 | * 46 | * @throws \Psr\Log\InvalidArgumentException 47 | */ 48 | public function log($level, $message, array $context = array()) 49 | { 50 | $this->doLog($level, $message, $context); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Debug/Psr3/CompatTrait_2.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.1 11 | */ 12 | 13 | namespace bdk\Debug\Psr3; 14 | 15 | /* 16 | Wrap in condition. 17 | PHPUnit code coverage scans all files and will conflict 18 | */ 19 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 20 | /** 21 | * Provide log method with signature compatible with psr/log v2 22 | * 23 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 24 | */ 25 | trait CompatTrait 26 | { 27 | /** 28 | * Logs with an arbitrary level. 29 | * 30 | * @param mixed $level debug, info, notice, warning, error, critical, alert, emergency 31 | * @param string|\Stringable $message message 32 | * @param mixed[] $context array 33 | * 34 | * @return void 35 | * 36 | * @throws \Psr\Log\InvalidArgumentException 37 | */ 38 | public function log($level, string|\Stringable $message, array $context = array()) 39 | { 40 | $this->doLog($level, $message, $context); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Debug/Psr3/CompatTrait_3.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.1 11 | */ 12 | 13 | namespace bdk\Debug\Psr3; 14 | 15 | /* 16 | Wrap in condition. 17 | PHPUnit code coverage scans all files and will conflict 18 | */ 19 | if (\trait_exists(__NAMESPACE__ . '\\CompatTrait', false) === false) { 20 | /** 21 | * Provide log method with signature compatible with psr/log v3 22 | * 23 | * @phpcs:disable Generic.Classes.DuplicateClassName.Found 24 | */ 25 | trait CompatTrait 26 | { 27 | /** 28 | * Logs with an arbitrary level. 29 | * 30 | * @param mixed $level debug, info, notice, warning, error, critical, alert, emergency 31 | * @param string|\Stringable $message message 32 | * @param mixed[] $context array 33 | * 34 | * @return void 35 | * 36 | * @throws \Psr\Log\InvalidArgumentException 37 | */ 38 | public function log($level, string|\Stringable $message, array $context = array()): void 39 | { 40 | $this->doLog($level, $message, $context); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Debug/Route/AbstractErrorRoute.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.4 11 | */ 12 | 13 | namespace bdk\Debug\Route; 14 | 15 | use bdk\Debug; 16 | use bdk\ErrorHandler; 17 | use bdk\ErrorHandler\Error; 18 | 19 | /** 20 | * common "shouldSend" method 21 | */ 22 | abstract class AbstractErrorRoute extends AbstractRoute 23 | { 24 | /** @var string */ 25 | protected $statsKey = ''; 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @param Debug $debug debug instance 31 | */ 32 | public function __construct(Debug $debug) 33 | { 34 | parent::__construct($debug); 35 | $this->cfg = \array_merge($this->cfg, array( 36 | 'errorMask' => E_ERROR | E_PARSE | E_COMPILE_ERROR | E_WARNING | E_USER_ERROR, 37 | )); 38 | $debug->errorHandler->setCfg('enableStats', true); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function getSubscriptions() 45 | { 46 | return array( 47 | ErrorHandler::EVENT_ERROR => ['onError', -1], 48 | ); 49 | } 50 | 51 | /** 52 | * ErrorHandler::EVENT_ERROR event subscriber 53 | * 54 | * @param Error $error error/event object 55 | * 56 | * @return void 57 | */ 58 | public function onError(Error $error) 59 | { 60 | if ($this->shouldSend($error, $this->statsKey) === false) { 61 | return; 62 | } 63 | $messages = $this->buildMessages($error); 64 | $this->sendMessages($messages); 65 | } 66 | 67 | /** 68 | * Build messages to send to client 69 | * 70 | * @param Error $error Error instance 71 | * 72 | * @return array 73 | */ 74 | abstract protected function buildMessages(Error $error); 75 | 76 | /** 77 | * Send messages to client (ie Discord, Slack, or Teams) 78 | * 79 | * @param array $messages array of message(s) to send to client 80 | * 81 | * @return void 82 | */ 83 | abstract protected function sendMessages(array $messages); 84 | 85 | /** 86 | * Should we send a notification for this error? 87 | * 88 | * @param Error $error Error instance 89 | * @param string $statsKey name under which we store stats 90 | * 91 | * @return bool 92 | */ 93 | private function shouldSend(Error $error, $statsKey) 94 | { 95 | if ($error['throw']) { 96 | // subscriber that set throw *should have* stopped error propagation 97 | return false; 98 | } 99 | if (($error['type'] & $this->cfg['errorMask']) !== $error['type']) { 100 | return false; 101 | } 102 | if ($error['isFirstOccur'] === false) { 103 | return false; 104 | } 105 | if ($error['inConsole']) { 106 | return false; 107 | } 108 | $error['stats'] = \array_merge(array( 109 | $statsKey => array( 110 | 'countSince' => 0, 111 | 'timestamp' => null, 112 | ), 113 | ), $error['stats'] ?: array()); 114 | $tsCutoff = \time() - $this->cfg['throttleMin'] * 60; 115 | if ($error['stats'][$statsKey]['timestamp'] > $tsCutoff) { 116 | // This error was recently sent 117 | $error['stats'][$statsKey]['countSince']++; 118 | return false; 119 | } 120 | $error['stats'][$statsKey]['timestamp'] = \time(); 121 | return true; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Debug/Route/Discord.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.0.4 11 | */ 12 | 13 | namespace bdk\Debug\Route; 14 | 15 | use bdk\CurlHttpMessage\Client as CurlHttpMessageClient; 16 | use bdk\Debug; 17 | use bdk\ErrorHandler\Error; 18 | use RuntimeException; 19 | 20 | /** 21 | * Send critical errors to Discord 22 | * 23 | * Not so much a route as a plugin (we only listen for errors) 24 | * 25 | * @see https://discord.com/developers/docs/resources/webhook#execute-webhook 26 | */ 27 | class Discord extends AbstractErrorRoute 28 | { 29 | protected $cfg = array( 30 | 'errorMask' => 0, 31 | 'onClientInit' => null, 32 | 'throttleMin' => 60, // 0 = no throttle 33 | 'webhookUrl' => null, // default pulled from DISCORD_WEBHOOK_URL env var 34 | ); 35 | 36 | /** @var CurlHttpMessageClient */ 37 | protected $client; 38 | 39 | protected $statsKey = 'discord'; 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | public function __construct(Debug $debug) 45 | { 46 | parent::__construct($debug); 47 | $this->cfg = \array_merge($this->cfg, array( 48 | 'webhookUrl' => \getenv('DISCORD_WEBHOOK_URL'), 49 | )); 50 | } 51 | 52 | /** 53 | * Validate configuration values 54 | * 55 | * @return void 56 | * 57 | * @throws RuntimeException 58 | */ 59 | private function assertCfg() 60 | { 61 | if ($this->cfg['webhookUrl']) { 62 | return; 63 | } 64 | throw new RuntimeException(\sprintf( 65 | '%s: missing config value: %s. Also tried env-var: %s', 66 | __CLASS__, 67 | 'webhookUrl', 68 | 'DISCORD_WEBHOOK_URL' 69 | )); 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | protected function buildMessages(Error $error) 76 | { 77 | $emoji = $error->isFatal() 78 | ? ':no_entry:' 79 | : ':warning:'; 80 | $message = array( 81 | 'content' => $emoji . ' **' . $error['typeStr'] . '**' . "\n" 82 | . $this->getRequestMethodUri() . "\n" 83 | . $error->getMessageText() . "\n" 84 | . $error['fileAndLine'], 85 | ); 86 | return [$message]; 87 | } 88 | 89 | /** 90 | * Return CurlHttpMessage 91 | * 92 | * @return CurlHttpMessageClient 93 | */ 94 | protected function getClient() 95 | { 96 | if ($this->client) { 97 | return $this->client; 98 | } 99 | $this->assertCfg(); 100 | $this->client = new CurlHttpMessageClient(); 101 | if (\is_callable($this->cfg['onClientInit'])) { 102 | \call_user_func($this->cfg['onClientInit'], $this->client); 103 | } 104 | return $this->client; 105 | } 106 | 107 | /** 108 | * {@inheritDoc} 109 | */ 110 | protected function sendMessages(array $messages) 111 | { 112 | foreach ($messages as $message) { 113 | $this->sendMessage($message); 114 | } 115 | } 116 | 117 | /** 118 | * Send message 119 | * 120 | * @param array $message Discord message 121 | * 122 | * @return void 123 | */ 124 | protected function sendMessage(array $message) 125 | { 126 | $client = $this->getClient(); 127 | $client->post( 128 | $this->cfg['webhookUrl'], 129 | array( 130 | 'Content-Type' => 'application/json; charset=utf-8', 131 | ), 132 | $message 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Debug/Route/RouteInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.1 11 | */ 12 | 13 | namespace bdk\Debug\Route; 14 | 15 | use bdk\Debug\LogEntry; 16 | use bdk\PubSub\Event; 17 | use bdk\PubSub\SubscriberInterface; 18 | 19 | /** 20 | * Route Interface 21 | * 22 | * Both processLogEntries and processLogEntry must be available for use, 23 | * although only one or the other will likely be used the interface 24 | * processLogEntries : log is processed at once 25 | * processLogEntry : log is processed one logEntry at a time 26 | */ 27 | interface RouteInterface extends SubscriberInterface 28 | { 29 | /** 30 | * Does this route append headers? 31 | * 32 | * @return bool 33 | */ 34 | public function appendsHeaders(); 35 | 36 | /** 37 | * Process log collectively (alerts, summary, log...) 38 | * likely implemented as a subscriber for the Debug::EVENT_OUTPUT event 39 | * 40 | * @param Event|null $event Event instance 41 | * 42 | * @return mixed 43 | */ 44 | public function processLogEntries($event = null); 45 | 46 | /** 47 | * Process log entry 48 | * 49 | * @param LogEntry $logEntry LogEntry instance 50 | * 51 | * @return mixed|void 52 | */ 53 | public function processLogEntry(LogEntry $logEntry); 54 | } 55 | -------------------------------------------------------------------------------- /src/Debug/Route/Text.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 2.0 11 | */ 12 | 13 | namespace bdk\Debug\Route; 14 | 15 | use bdk\Debug; 16 | 17 | /** 18 | * Output log as plain-text 19 | */ 20 | class Text extends AbstractRoute 21 | { 22 | /** 23 | * Constructor 24 | * 25 | * @param Debug $debug debug instance 26 | */ 27 | public function __construct(Debug $debug) 28 | { 29 | parent::__construct($debug); 30 | if (!$this->dumper) { 31 | $this->dumper = $debug->getDump('text'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Debug/Route/WampHelper.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Route; 14 | 15 | use bdk\Debug; 16 | 17 | /** 18 | * WAMP Helper Methods 19 | */ 20 | class WampHelper 21 | { 22 | /** @var Debug */ 23 | public $debug; 24 | 25 | /** 26 | * Constructor 27 | * 28 | * @param Debug $debug Debug instance 29 | */ 30 | public function __construct(Debug $debug) 31 | { 32 | $this->debug = $debug; 33 | } 34 | 35 | /** 36 | * Get meta values to publish 37 | * 38 | * @return array 39 | */ 40 | public function getMeta() 41 | { 42 | $default = array( 43 | 'argv' => array(), 44 | 'DOCUMENT_ROOT' => null, 45 | 'HTTPS' => null, 46 | 'HTTP_HOST' => null, 47 | 'processId' => \getmypid(), 48 | 'REMOTE_ADDR' => null, 49 | 'REQUEST_METHOD' => $this->debug->serverRequest->getMethod(), 50 | 'REQUEST_TIME' => null, 51 | 'REQUEST_URI' => \urldecode($this->debug->serverRequest->getRequestTarget()), 52 | 'SERVER_ADDR' => null, 53 | 'SERVER_NAME' => null, 54 | ); 55 | $metaVals = \array_merge( 56 | $default, 57 | $this->debug->serverRequest->getServerParams() 58 | ); 59 | $metaVals = \array_intersect_key($metaVals, $default); 60 | if ($this->debug->isCli()) { 61 | $metaVals['REQUEST_METHOD'] = null; 62 | $metaVals['REQUEST_URI'] = '$: ' . \implode(' ', $metaVals['argv']); 63 | } 64 | unset($metaVals['argv']); 65 | return $this->debug->redact($metaVals); 66 | } 67 | 68 | /** 69 | * Get config values that are published with meta info 70 | * 71 | * @return array 72 | */ 73 | public function getMetaConfig() 74 | { 75 | return array( 76 | 'channelNameRoot' => $this->debug->rootInstance->getCfg('channelName', Debug::CONFIG_DEBUG), 77 | 'debugVersion' => Debug::VERSION, 78 | 'drawer' => $this->debug->getCfg('routeHtml.drawer'), 79 | 'interface' => $this->debug->getInterface(), 80 | 'linkFilesTemplateDefault' => \strtr( 81 | \ini_get('xdebug.file_link_format'), 82 | array( 83 | '%f' => '%file', 84 | '%l' => '%line', 85 | ) 86 | ) ?: null, 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Debug/Utility/HtmlParse.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Utility; 14 | 15 | /** 16 | * Html utilities 17 | */ 18 | class HtmlParse 19 | { 20 | /** 21 | * Parse attribute value 22 | * 23 | * @param string $name attribute name 24 | * @param string $val value (assumed to be html encoded) 25 | * @param int $options bitmask of Html::PARSE_ATTRIB_x FLAGS 26 | * 27 | * @return mixed 28 | */ 29 | public static function parseAttribValue($name, $val, $options) 30 | { 31 | $val = \htmlspecialchars_decode($val); 32 | if ($name === 'class') { 33 | $decode = (bool) ($options & Html::PARSE_ATTRIB_CLASS); 34 | return self::parseAttribClass($val, $decode); 35 | } 36 | if (\substr($name, 0, 5) === 'data-') { 37 | $decode = (bool) ($options & Html::PARSE_ATTRIB_DATA); 38 | return self::parseAttribData($val, $decode); 39 | } 40 | if (\in_array($name, Html::$htmlBoolAttr, true)) { 41 | return true; 42 | } 43 | if (\in_array($name, Html::$htmlBoolAttrEnum, true)) { 44 | return self::parseAttribBoolEnum($val); 45 | } 46 | if (\is_numeric($val)) { 47 | $decode = (bool) ($options & Html::PARSE_ATTRIB_NUMERIC); 48 | return self::parseAttribNumeric($val, $decode); 49 | } 50 | return $val; 51 | } 52 | 53 | /** 54 | * Convert bool enum attribute value to bool 55 | * 56 | * @param string $val enum attribute value 57 | * 58 | * @return bool|string 59 | */ 60 | private static function parseAttribBoolEnum($val) 61 | { 62 | $parsed = \filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); 63 | return $parsed !== null 64 | ? $parsed 65 | : $val; 66 | } 67 | 68 | /** 69 | * Convert class attribute value to array of classes 70 | * 71 | * @param string $val attribute value to decode 72 | * @param bool $asArray whether to return array 73 | * 74 | * @return array|string 75 | */ 76 | private static function parseAttribClass($val, $asArray) 77 | { 78 | if (!$asArray) { 79 | return $val; 80 | } 81 | $classes = \explode(' ', $val); 82 | \sort($classes); 83 | return \array_unique($classes); 84 | } 85 | 86 | /** 87 | * Json decode data-xxx attribute 88 | * 89 | * @param string $val attribute value to decode 90 | * @param bool $decode whether to decode 91 | * 92 | * @return mixed 93 | */ 94 | private static function parseAttribData($val, $decode) 95 | { 96 | if (!$decode) { 97 | return $val; 98 | } 99 | $decoded = \json_decode((string) $val, true); 100 | if ($decoded === null && $val !== 'null') { 101 | $decoded = \json_decode('"' . $val . '"', true); 102 | } 103 | return $decoded; 104 | } 105 | 106 | /** 107 | * Convert numeric attribute value to float/int 108 | * 109 | * @param numeric $val enum attribute value 110 | * @param bool $decode whether to cast to int/float 111 | * 112 | * @return string|float|int 113 | */ 114 | private static function parseAttribNumeric($val, $decode) 115 | { 116 | return $decode 117 | ? \json_decode($val) 118 | : $val; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Debug/Utility/PhpDoc/Helper.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Utility\PhpDoc; 14 | 15 | /** 16 | * PhpDoc parsing helper methods 17 | * 18 | * @psalm-import-type TagInfo from \bdk\Debug\Utility\PhpDoc 19 | */ 20 | class Helper 21 | { 22 | /** 23 | * Split description and summary 24 | * 25 | * @param string $comment Beginning of doc comment 26 | * 27 | * @return array{desc?:string,summary?:string} 28 | */ 29 | public static function parseDescSummary($comment) 30 | { 31 | /* 32 | Do some string replacement 33 | */ 34 | $comment = \preg_replace('/^\\\@/m', '@', $comment); 35 | $comment = \str_replace('{@*}', '*/', $comment); 36 | /* 37 | split into summary & description 38 | summary ends with empty whitespace line or "." followed by \n 39 | */ 40 | $split = \preg_split('/(\.[\r\n]+|[\r\n]{2})/', $comment, 2, PREG_SPLIT_DELIM_CAPTURE); 41 | $split = \array_replace(['', '', ''], $split); 42 | // assume that summary and desc won't be "0".. remove empty value and merge 43 | return \array_filter(array( 44 | 'desc' => self::trimDesc($split[2]), 45 | 'summary' => \trim($split[0] . $split[1]), // split[1] is the ".\n" 46 | )); 47 | } 48 | 49 | /** 50 | * Trim leading spaces from each description line 51 | * 52 | * @param string $desc string to trim 53 | * 54 | * @return string 55 | */ 56 | public static function trimDesc($desc) 57 | { 58 | $desc = \rtrim((string) $desc); 59 | $lines = \explode("\n", $desc); 60 | $leadingSpaces = array(); 61 | $trimLineStart = 0; 62 | // collect leadingSpaces on non-empty lines 63 | foreach (\array_filter($lines) as $i => $line) { 64 | $leadingSpaces[$i] = \strspn($line, ' '); 65 | } 66 | if (\count($leadingSpaces) === 0) { 67 | // no non-empty lines 68 | return ''; 69 | } 70 | $lines = \array_slice($lines, \key($leadingSpaces)); // start with first non-empty line 71 | if (\reset($leadingSpaces) === 0) { 72 | // first non-empty line has no leading spaces (ie a line-wrapped param description) 73 | $trimLineStart = 1; 74 | \array_shift($leadingSpaces); // don't include first line when determining trimLen 75 | } elseif (\min($leadingSpaces) === 4) { 76 | // special case where desc contains only code example(s) 77 | return $desc; 78 | } 79 | $trimLen = $leadingSpaces 80 | ? \min($leadingSpaces) 81 | : 0; 82 | for ($i = $trimLineStart, $count = \count($lines); $i < $count; $i++) { 83 | $lines[$i] = \substr($lines[$i], $trimLen); 84 | } 85 | return \implode("\n", $lines); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Debug/Utility/PhpDoc/ParseParam.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Utility\PhpDoc; 14 | 15 | /** 16 | * Parse 'param', 'property', 'property-read', 'property-write', & 'var' 17 | * 18 | * @psalm-import-type TagInfo from \bdk\Debug\Utility\PhpDoc 19 | */ 20 | class ParseParam 21 | { 22 | /** 23 | * Parse @method tag 24 | * 25 | * @param array $parsed type, name, & desc 26 | * @param array $info tagName, raw tag string, etc 27 | * 28 | * @return array 29 | * 30 | * @psalm-param TagInfo $info 31 | */ 32 | public function __invoke(array $parsed, array $info) 33 | { 34 | $tagName = $info['tagName']; 35 | if (self::strStartsWithVariable($parsed['desc'])) { 36 | \preg_match('/^(\S*)/', $parsed['desc'], $matches); 37 | $parsed['name'] = $matches[1]; 38 | $parsed['desc'] = \preg_replace('/^\S*\s+/', '', $parsed['desc']); 39 | } 40 | if ($tagName === 'param' && $parsed['name'] === null && \strpos((string) $parsed['desc'], ' ') === false) { 41 | $parsed['name'] = $parsed['desc']; 42 | $parsed['desc'] = ''; 43 | } 44 | if ($tagName === 'param') { 45 | $parsed['isVariadic'] = \strpos((string) $parsed['name'], '...') !== false; 46 | } 47 | if ($parsed['name']) { 48 | $parsed['name'] = \trim($parsed['name'], '&$,.'); 49 | } 50 | if ($tagName === 'var' && $info['elementName'] !== null && $parsed['name'] !== $info['elementName']) { 51 | // name mismatch 52 | $parsed['desc'] = \trim($parsed['name'] . ' ' . $parsed['desc']); 53 | $parsed['name'] = $info['elementName']; 54 | } 55 | $parsed['type'] = $info['phpDoc']->type->normalize($parsed['type'], $info['className'], $info['fullyQualifyType']); 56 | return $parsed; 57 | } 58 | 59 | /** 60 | * Test if string appears to start with a variable name 61 | * 62 | * @param string $str String to test 63 | * 64 | * @return bool 65 | */ 66 | private static function strStartsWithVariable($str) 67 | { 68 | if ($str === null) { 69 | return false; 70 | } 71 | return \strpos($str, '$') === 0 72 | || \strpos($str, '&$') === 0 73 | || \strpos($str, '...$') === 0 74 | || \strpos($str, '&...$') === 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Debug/Utility/SqlQueryAnalysis.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since 3.3 11 | */ 12 | 13 | namespace bdk\Debug\Utility; 14 | 15 | use bdk\Debug; 16 | use Closure; 17 | 18 | /** 19 | * Test SQL queries for common performance issues 20 | */ 21 | class SqlQueryAnalysis 22 | { 23 | /** @var Debug */ 24 | private $debug; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param Debug $debug Debug instance 30 | */ 31 | public function __construct(Debug $debug) 32 | { 33 | $this->debug = $debug; 34 | } 35 | 36 | /** 37 | * Find common query performance issues 38 | * 39 | * @param string $sql SQL query 40 | * 41 | * @return void 42 | * 43 | * @link https://github.com/rap2hpoutre/mysql-xplain-xplain/blob/master/app/Explainer.php 44 | */ 45 | public function analyze($sql) 46 | { 47 | \array_map([$this, 'performQueryAnalysisTest'], [ 48 | [\preg_match('/^\s*SELECT\s*`?[a-zA-Z0-9]*`?\.?\*/i', $sql) === 1, 49 | 'Use %cSELECT *%c only if you need all columns from table', 50 | ], 51 | [\stripos($sql, 'ORDER BY RAND()') !== false, 52 | '%cORDER BY RAND()%c is slow, avoid if you can.', 53 | ], 54 | [\strpos($sql, '!=') !== false, 55 | 'The %c!=%c operator is not standard. Use the %c<>%c operator instead.', 56 | ], 57 | [\preg_match('/^SELECT\s/i', $sql) && \stripos($sql, 'WHERE') === false, 58 | 'The %cSELECT%c statement has no %cWHERE%c clause and could examine many more rows than intended', 59 | ], 60 | static function () use ($sql) { 61 | $matches = []; 62 | return \preg_match('/LIKE\s+[\'"](%.*?)[\'"]/i', $sql, $matches) 63 | ? 'An argument has a leading wildcard character: %c' . $matches[1] . '%c and cannot use an index if one exists.' 64 | : false; 65 | }, 66 | [\preg_match('/LIMIT\s/i', $sql) && \stripos($sql, 'ORDER BY') === false, 67 | '%cLIMIT%c without %cORDER BY%c causes non-deterministic results', 68 | ], 69 | ]); 70 | } 71 | 72 | /** 73 | * Process query analysis test and log result if test fails 74 | * 75 | * @param array|Closure $test query test 76 | * 77 | * @return void 78 | * 79 | * @SuppressWarnings(PHPMD.UnusedPrivateMethod) 80 | */ 81 | private function performQueryAnalysisTest($test) 82 | { 83 | if ($test instanceof Closure) { 84 | $test = $test(); 85 | $test = [ 86 | $test, 87 | $test, 88 | ]; 89 | } 90 | if ($test[0] === false) { 91 | return; 92 | } 93 | $params = [ 94 | $test[1], 95 | ]; 96 | $cCount = \substr_count($params[0], '%c'); 97 | for ($i = 0; $i < $cCount; $i += 2) { 98 | $params[] = 'font-family:monospace'; 99 | $params[] = ''; 100 | } 101 | $params[] = $this->debug->meta('uncollapse', false); 102 | \call_user_func_array([$this->debug, 'warn'], $params); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Debug/css/PrismJsLightDark.css: -------------------------------------------------------------------------------- 1 | .debug{--color: #33a;--colorBg: #f5f2f0;--textShadow: #fff;--comment: #6e6e6e;--punctuation: #4e4e4e;--property: #905;--operator: #70b;--selector: #487b00;--url: #8d6640;--urlBg: hsla(0, 0%, 100%, .5);--boolean: #905;--atRule: #0075a8;--keyword: #0075a8;--null: #999;--function: #c93654;--regex: #860}.debug[data-theme=dark]{--color: #6ae;--colorBg: #222;--textShadow: #000;--comment: #9ab;--punctuation: #ccc;--property: #e70;--operator: #d7f;--selector: #8b2;--url: #cde;--urlBg: rgba(0,0,0,.5);--boolean: #a8f;--atRule: #ffb;--keyword: #fe6;--function: #f55;--regex: #f91}code[class*=language-],pre[class*=language-]{color:var(--color);background:none;text-shadow:0 1px var(--textShadow);font-family:Consolas,Monaco,"Andale Mono","Ubuntu Mono",monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,code[class*=language-] ::-moz-selection{text-shadow:none;background:#b3d4fc}pre[class*=language-]::selection,pre[class*=language-] ::selection,code[class*=language-]::selection,code[class*=language-] ::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:var(--colorBg)}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.atrule,.token.attr-value,.token.keyword{color:var(--atRule)}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:var(--comment)}.token.punctuation{color:var(--punctuation)}.token.namespace{opacity:.7}.token.boolean{color:var(--boolean)}.token.null{color:var(--null)}.token.number{color:var(--color-numeric)}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:var(--property)}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:var(--selector)}.token.operator{color:var(--operator)}.token.entity,.token.url,.language-css .token.string,.style .token.string{color:var(--url);background:var(--urlBg)}.token.function,.token.class-name{color:var(--function)}.token.regex,.token.important,.token.variable{color:var(--regex)}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.entity{cursor:help}pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right, hsla(24, 20%, 50%, 0.1) 70%, hsla(24, 20%, 50%, 0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:hsl(24,20%,95%);font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:before,.line-numbers .line-highlight:after{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right} 2 | -------------------------------------------------------------------------------- /src/Debug/js/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html?#themes=prism&languages=markup+css+clike+javascript+javadoclike+json+markup-templating+php+phpdoc+php-extras+sql&plugins=line-highlight+line-numbers */ 3 | code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)} 5 | pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right} 6 | -------------------------------------------------------------------------------- /src/ErrorHandler/Plugin/Stats.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2014-2025 Brad Kent 8 | * @since v3.2 9 | */ 10 | 11 | namespace bdk\ErrorHandler\Plugin; 12 | 13 | use bdk\ErrorHandler; 14 | use bdk\ErrorHandler\AbstractComponent; 15 | use bdk\ErrorHandler\Error; 16 | use bdk\ErrorHandler\Plugin\StatsStoreFile; 17 | use bdk\ErrorHandler\Plugin\StatsStoreInterface; 18 | use bdk\PubSub\SubscriberInterface; 19 | 20 | /** 21 | * Keep track of when errors were last emailed or other 22 | * 23 | * @property array $summaryErrors 24 | */ 25 | class Stats extends AbstractComponent implements SubscriberInterface 26 | { 27 | /** @var StatsStoreInterface */ 28 | protected $dataStore; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param array $cfg Configuration 34 | */ 35 | public function __construct($cfg = array()) 36 | { 37 | $this->cfg = array( 38 | 'dataStoreFactory' => function () { 39 | $cfgDataStore = \array_diff_key($this->cfg, array('dataStoreFactory' => null)); 40 | return new StatsStoreFile($cfgDataStore); 41 | }, 42 | ); 43 | $this->setCfg(\array_merge($this->cfg, $cfg)); 44 | } 45 | 46 | /** 47 | * Get stats for given error 48 | * 49 | * @param string|Error $errorOrHash error-hash, or Error instance 50 | * 51 | * @return array returns empty array if no stats 52 | */ 53 | public function find($errorOrHash) 54 | { 55 | return $errorOrHash instanceof Error 56 | ? $this->dataStore->findByError($errorOrHash) 57 | : $this->dataStore->findByHash($errorOrHash); 58 | } 59 | 60 | /** 61 | * Clear data 62 | * 63 | * @return void 64 | */ 65 | public function flush() 66 | { 67 | $this->dataStore->flush(); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function getSubscriptions() 74 | { 75 | return array( 76 | ErrorHandler::EVENT_ERROR => [ 77 | ['onErrorHighPri', PHP_INT_MAX], 78 | ['onErrorLowPri', PHP_INT_MAX * -1], 79 | ], 80 | ); 81 | } 82 | 83 | /** 84 | * Return list of errors that have 85 | * not occurred since their cutoff 86 | * have occurred since their last email 87 | * 88 | * @return array 89 | */ 90 | public function getSummaryErrors() 91 | { 92 | return $this->dataStore->getSummaryErrors(); 93 | } 94 | 95 | /** 96 | * Initialize stats on error 97 | * 98 | * @param Error $error Error instance 99 | * 100 | * @return void 101 | */ 102 | public function onErrorHighPri(Error $error) 103 | { 104 | $error['stats'] = array( 105 | 'count' => 1, 106 | 'tsAdded' => \time(), 107 | 'tsLastOccur' => null, 108 | ); 109 | $errorStats = $this->dataStore->findByError($error); 110 | if ($errorStats) { 111 | unset($errorStats['info']); 112 | $errorStats['count']++; 113 | $error['stats'] = \array_merge($error['stats'], $errorStats); 114 | } 115 | } 116 | 117 | /** 118 | * Save the stats to file 119 | * 120 | * @param Error $error Error instance 121 | * 122 | * @return void 123 | */ 124 | public function onErrorLowPri(Error $error) 125 | { 126 | $this->dataStore->errorUpsert($error); 127 | } 128 | 129 | /** 130 | * Handle updated cfg values 131 | * 132 | * @param array $cfg new config values 133 | * @param array $prev previous config values 134 | * 135 | * @return void 136 | */ 137 | protected function postSetCfg($cfg = array(), $prev = array()) 138 | { 139 | $cfgDataStore = \array_diff_key($cfg, array('dataStoreFactory' => null)); 140 | if (isset($cfg['dataStoreFactory'])) { 141 | $this->dataStore = $cfg['dataStoreFactory']($cfgDataStore); 142 | } elseif ($cfgDataStore) { 143 | $this->dataStore->setCfg($cfgDataStore); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/ErrorHandler/Plugin/StatsStoreInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2014-2025 Brad Kent 8 | * @since v3.2 9 | */ 10 | 11 | namespace bdk\ErrorHandler\Plugin; 12 | 13 | use bdk\ErrorHandler\Error; 14 | 15 | /** 16 | * Interface for storing and retrieving error statistics 17 | */ 18 | interface StatsStoreInterface 19 | { 20 | /** 21 | * Adds/Update/Store this error's stats 22 | * 23 | * @param Error $error Error instance 24 | * 25 | * @return bool 26 | */ 27 | public function errorUpsert(Error $error); 28 | 29 | /** 30 | * Get stats for given error 31 | * 32 | * @param Error $error Error instance 33 | * 34 | * @return array returns empty array if no stats 35 | */ 36 | public function findByError(Error $error); 37 | 38 | /** 39 | * Get stats for given error hash 40 | * 41 | * @param string $hash Error hash string 42 | * 43 | * @return array returns empty array if no stats 44 | */ 45 | public function findByHash($hash); 46 | 47 | /** 48 | * Clear data 49 | * 50 | * @return void 51 | */ 52 | public function flush(); 53 | 54 | /** 55 | * Return list of errors that have 56 | * not occurred since their cutoff 57 | * have occurred since their last email 58 | * 59 | * @return array 60 | */ 61 | public function getSummaryErrors(); 62 | } 63 | -------------------------------------------------------------------------------- /src/Promise/Create.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise; 11 | 12 | use ArrayIterator; 13 | use bdk\Promise; 14 | use bdk\Promise\Exception\RejectionException; 15 | use bdk\Promise\PromiseInterface; 16 | use Exception; 17 | use Iterator; 18 | use Throwable; 19 | 20 | /** 21 | * Internal Helper methods 22 | */ 23 | final class Create 24 | { 25 | /** 26 | * Creates a promise for a value if the value is not a promise. 27 | * 28 | * @param mixed $value Promise or value. 29 | * 30 | * @return PromiseInterface 31 | */ 32 | public static function promiseFor($value) 33 | { 34 | if ($value instanceof PromiseInterface) { 35 | return $value; 36 | } 37 | 38 | $isThenable = \is_object($value) && \method_exists($value, 'then'); 39 | if ($isThenable === false) { 40 | return new FulfilledPromise($value); 41 | } 42 | 43 | // Return a new promise that shadows the given thenable. 44 | $waitFn = \method_exists($value, 'wait') ? [$value, 'wait'] : null; 45 | $cancelFn = \method_exists($value, 'cancel') ? [$value, 'cancel'] : null; 46 | $promise = new Promise($waitFn, $cancelFn); 47 | $value->then([$promise, 'resolve'], [$promise, 'reject']); 48 | return $promise; 49 | } 50 | 51 | /** 52 | * Creates a rejected promise for a reason if the reason is not a promise. 53 | * If the provided reason is a promise, then it is returned as-is. 54 | * 55 | * @param mixed $reason Promise or reason. 56 | * 57 | * @return PromiseInterface 58 | */ 59 | public static function rejectionFor($reason) 60 | { 61 | if ($reason instanceof PromiseInterface) { 62 | return $reason; 63 | } 64 | return new RejectedPromise($reason); 65 | } 66 | 67 | /** 68 | * Create an exception for a rejected promise value. 69 | * 70 | * @param mixed $reason Exception or Reason 71 | * 72 | * @return Exception|Throwable 73 | */ 74 | public static function exceptionFor($reason) 75 | { 76 | return $reason instanceof Exception || $reason instanceof Throwable 77 | ? $reason 78 | : new RejectionException($reason); 79 | } 80 | 81 | /** 82 | * Returns an iterator for the given value. 83 | * 84 | * @param mixed $value Value to Iterator-ify 85 | * 86 | * @return Iterator 87 | */ 88 | public static function iteratorFor($value) 89 | { 90 | if ($value instanceof Iterator) { 91 | return $value; 92 | } 93 | 94 | if (\is_array($value)) { 95 | return new ArrayIterator($value); 96 | } 97 | 98 | return new ArrayIterator([$value]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Promise/Exception/AggregateException.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise\Exception; 11 | 12 | use bdk\Promise\Exception\RejectionException; 13 | 14 | /** 15 | * Exception thrown when too many errors occur in the some() or any() methods. 16 | */ 17 | class AggregateException extends RejectionException 18 | { 19 | /** 20 | * Constructor 21 | * 22 | * @param string $msg Exception message 23 | * @param array $reasons Reasons 24 | */ 25 | public function __construct($msg, array $reasons) 26 | { 27 | parent::__construct( 28 | $reasons, 29 | \sprintf('%s; %d rejected promises', $msg, \count($reasons)) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Promise/Exception/CancellationException.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise\Exception; 11 | 12 | use bdk\Promise\Exception\RejectionException; 13 | 14 | /** 15 | * Exception that is set as the reason for a promise that has been cancelled. 16 | */ 17 | class CancellationException extends RejectionException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Promise/Exception/RejectionException.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise\Exception; 11 | 12 | use JsonSerializable; 13 | use RuntimeException; 14 | 15 | /** 16 | * A special exception that is thrown when waiting on a rejected promise. 17 | * 18 | * The reason value is available via the getReason() method. 19 | */ 20 | class RejectionException extends RuntimeException 21 | { 22 | /** @var mixed Rejection reason(s) */ 23 | private $reason; 24 | 25 | /** 26 | * @param mixed $reason Rejection reason(s). 27 | * @param string $description Optional description 28 | */ 29 | public function __construct($reason, $description = null) 30 | { 31 | $this->reason = $reason; 32 | 33 | $message = 'The promise was rejected'; 34 | 35 | if ($description) { 36 | $message .= ' with reason: ' . $description; 37 | } elseif ( 38 | \is_string($reason) 39 | || (\is_object($reason) && \method_exists($reason, '__toString')) 40 | ) { 41 | $message .= ' with reason: ' . $this->reason; 42 | } elseif ($reason instanceof JsonSerializable) { 43 | $message .= ' with reason: ' 44 | . \json_encode($this->reason, JSON_PRETTY_PRINT); 45 | } 46 | 47 | parent::__construct($message); 48 | } 49 | 50 | /** 51 | * Returns the rejection reason(s). 52 | * 53 | * @return mixed 54 | */ 55 | public function getReason() 56 | { 57 | return $this->reason; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Promise/FulfilledPromise.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise; 11 | 12 | use bdk\Promise; 13 | use Exception; 14 | use InvalidArgumentException; 15 | use Throwable; 16 | 17 | /** 18 | * A promise that has been fulfilled. 19 | * 20 | * Thening off of this promise will invoke the onFulfilled callback 21 | * immediately and ignore other callbacks. 22 | */ 23 | class FulfilledPromise extends Promise // implements PromiseInterface 24 | { 25 | /** 26 | * Constructor 27 | * 28 | * @param mixed $value Resolved value 29 | * 30 | * @throws InvalidArgumentException 31 | */ 32 | public function __construct($value) 33 | { 34 | if (\is_object($value) && \method_exists($value, 'then')) { 35 | throw new InvalidArgumentException( 36 | 'You cannot create a FulfilledPromise with a promise.' 37 | ); 38 | } 39 | 40 | $this->result = $value; 41 | $this->state = self::FULFILLED; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function then($onFulfilled = null, $onRejected = null) 48 | { 49 | \bdk\Promise\Utils::assertType($onFulfilled, 'callable'); 50 | \bdk\Promise\Utils::assertType($onRejected, 'callable'); 51 | 52 | // Return self if there is no onFulfilled function. 53 | if (!$onFulfilled) { 54 | return $this; 55 | } 56 | 57 | $queue = self::queue(); 58 | $result = $this->result; 59 | $promise = new Promise([$queue, 'run']); 60 | $queue->add(static function () use ($promise, $result, $onFulfilled) { 61 | if ($promise->isSettled()) { 62 | return; 63 | } 64 | try { 65 | // Return a resolved promise if onFulfilled does not throw. 66 | $promise->resolve($onFulfilled($result)); 67 | } catch (Throwable $e) { 68 | // onFulfilled threw, so return a rejected promise. 69 | $promise->reject($e); 70 | } catch (Exception $e) { 71 | // onFulfilled threw, so return a rejected promise. 72 | $promise->reject($e); 73 | } 74 | }); 75 | 76 | return $promise; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Promise/Is.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise; 11 | 12 | use bdk\Promise\PromiseInterface; 13 | 14 | /** 15 | * State test methods 16 | */ 17 | final class Is 18 | { 19 | /** 20 | * Returns true if a promise is pending. 21 | * 22 | * @param PromiseInterface $promise Promise to check 23 | * 24 | * @return bool 25 | */ 26 | public static function pending(PromiseInterface $promise) 27 | { 28 | return $promise->getState() === PromiseInterface::PENDING; 29 | } 30 | 31 | /** 32 | * Returns true if a promise is fulfilled or rejected. 33 | * 34 | * @param PromiseInterface $promise Promise to check 35 | * 36 | * @return bool 37 | */ 38 | public static function settled(PromiseInterface $promise) 39 | { 40 | return $promise->getState() !== PromiseInterface::PENDING; 41 | } 42 | 43 | /** 44 | * Returns true if a promise is fulfilled. 45 | * 46 | * @param PromiseInterface $promise Promise to check 47 | * 48 | * @return bool 49 | */ 50 | public static function fulfilled(PromiseInterface $promise) 51 | { 52 | return $promise->getState() === PromiseInterface::FULFILLED; 53 | } 54 | 55 | /** 56 | * Returns true if a promise is rejected. 57 | * 58 | * @param PromiseInterface $promise Promise to check 59 | * 60 | * @return bool 61 | */ 62 | public static function rejected(PromiseInterface $promise) 63 | { 64 | return $promise->getState() === PromiseInterface::REJECTED; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Promise/PromiseInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise; 11 | 12 | /** 13 | * A promise represents the eventual result of an asynchronous operation. 14 | * 15 | * The primary way of interacting with a promise is through its then method, 16 | * which registers callbacks to receive either a promise’s eventual value or 17 | * the reason why the promise cannot be fulfilled. 18 | * 19 | * @link https://promisesaplus.com/ 20 | */ 21 | interface PromiseInterface 22 | { 23 | const PENDING = 'pending'; 24 | const FULFILLED = 'fulfilled'; 25 | const REJECTED = 'rejected'; 26 | 27 | /** 28 | * Appends fulfillment and rejection handlers to the promise, and returns 29 | * a new promise resolving to the return value of the called handler. 30 | * 31 | * @param callable|null $onFulfilled Invoked when the promise fulfills. 32 | * @param callable|null $onRejected Invoked when the promise is rejected. 33 | * 34 | * @return PromiseInterface 35 | */ 36 | public function then($onFulfilled = null, $onRejected = null); 37 | 38 | /** 39 | * Appends a rejection handler callback to the promise, and returns a new 40 | * promise resolving to the return value of the callback if it is called, 41 | * or to its original fulfillment value if the promise is instead 42 | * fulfilled. 43 | * 44 | * @param callable $onRejected Invoked when the promise is rejected. 45 | * 46 | * @return PromiseInterface 47 | */ 48 | public function otherwise(callable $onRejected); 49 | 50 | /** 51 | * Get the state of the promise ("pending", "rejected", or "fulfilled"). 52 | * 53 | * The three states can be checked against the constants defined on 54 | * PromiseInterface: PENDING, FULFILLED, and REJECTED. 55 | * 56 | * @return string 57 | */ 58 | public function getState(); 59 | 60 | /** 61 | * Resolve the promise with the given value. 62 | * 63 | * @param mixed $value Value promise should return 64 | * 65 | * @return void 66 | * 67 | * @throws \RuntimeException if the promise is already resolved. 68 | */ 69 | public function resolve($value); 70 | 71 | /** 72 | * Reject the promise with the given reason. 73 | * 74 | * @param mixed $reason reason value / message / exception 75 | * 76 | * @return void 77 | * 78 | * @throws \RuntimeException if the promise is already resolved. 79 | */ 80 | public function reject($reason); 81 | 82 | /** 83 | * Cancels the promise if possible. 84 | * 85 | * @link https://github.com/promises-aplus/cancellation-spec/issues/7 86 | * 87 | * @return void 88 | */ 89 | public function cancel(); 90 | 91 | /** 92 | * Waits until the promise completes if possible. 93 | * 94 | * If the promise cannot be waited on, then the promise will be rejected. 95 | * 96 | * @param bool $unwrap (true) unwrap the result of the promise, either returning 97 | * the resolved value or throwing the rejected exception. 98 | * 99 | * @return mixed 100 | * 101 | * @throws \LogicException if the promise has no wait function or if the 102 | * promise does not settle after waiting. 103 | */ 104 | public function wait($unwrap = true); 105 | } 106 | -------------------------------------------------------------------------------- /src/Promise/RejectedPromise.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise; 11 | 12 | use bdk\Promise; 13 | use Exception; 14 | use InvalidArgumentException; 15 | use Throwable; 16 | 17 | /** 18 | * A promise that has been rejected. 19 | * 20 | * Thening off of this promise will invoke the onRejected callback 21 | * immediately and ignore other callbacks. 22 | */ 23 | class RejectedPromise extends Promise 24 | { 25 | /** 26 | * Constructor 27 | * 28 | * @param mixed $reason rejection reason 29 | * 30 | * @throws InvalidArgumentException 31 | */ 32 | public function __construct($reason) 33 | { 34 | if (\is_object($reason) && \method_exists($reason, 'then')) { 35 | throw new InvalidArgumentException( 36 | 'You cannot create a RejectedPromise with a promise.' 37 | ); 38 | } 39 | $this->result = $reason; 40 | $this->state = self::REJECTED; 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | public function then($onFulfilled = null, $onRejected = null) 47 | { 48 | \bdk\Promise\Utils::assertType($onFulfilled, 'callable'); 49 | \bdk\Promise\Utils::assertType($onRejected, 'callable'); 50 | 51 | // Return self if there is no onRejected function. 52 | if (!$onRejected) { 53 | return $this; 54 | } 55 | [$onFulfilled]; // suppress unused 56 | 57 | $queue = self::queue(); 58 | $result = $this->result; 59 | $promise = new Promise([$queue, 'run']); 60 | $queue->add(static function () use ($promise, $result, $onRejected) { 61 | if ($promise->isSettled()) { 62 | return; 63 | } 64 | try { 65 | // Return a resolved promise if onRejected does not throw. 66 | $promise->resolve($onRejected($result)); 67 | } catch (Throwable $e) { 68 | // onRejected threw, so return a rejected promise. 69 | $promise->reject($e); 70 | } catch (Exception $e) { 71 | // onRejected threw, so return a rejected promise. 72 | $promise->reject($e); 73 | } 74 | }); 75 | 76 | return $promise; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Promise/TaskQueue.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Promise; 11 | 12 | /** 13 | * A task queue that executes tasks in a FIFO order. 14 | * 15 | * This task queue class is used to settle promises asynchronously and 16 | * maintains a constant stack size. You can use the task queue asynchronously 17 | * by calling the `run()` function of the global task queue in an event loop. 18 | * 19 | * bdk\Promise::queue()->run(); 20 | */ 21 | class TaskQueue 22 | { 23 | /** @var bool */ 24 | private $enableShutdown = true; 25 | 26 | /** @var callable[] */ 27 | private $queue = array(); 28 | 29 | /** 30 | * Constructor 31 | * 32 | * @param bool $withShutdown Process the queue on shutdown? 33 | */ 34 | public function __construct($withShutdown = true) 35 | { 36 | if ($withShutdown) { 37 | \register_shutdown_function([$this, 'onShutdown']); 38 | } 39 | } 40 | 41 | /** 42 | * Is the queue empty? 43 | * 44 | * @return bool 45 | */ 46 | public function isEmpty() 47 | { 48 | return !$this->queue; 49 | } 50 | 51 | /** 52 | * Adds a task to the queue that will be executed when run() is called. 53 | * 54 | * @param callable $task callable to run 55 | * 56 | * @return void 57 | */ 58 | public function add(callable $task) 59 | { 60 | $this->queue[] = $task; 61 | } 62 | 63 | /** 64 | * Execute all of the pending task in the queue. 65 | * 66 | * @return void 67 | */ 68 | public function run() 69 | { 70 | while ($task = \array_shift($this->queue)) { 71 | /** @var callable $task */ 72 | $task(); 73 | } 74 | } 75 | 76 | /** 77 | * The task queue will be run and exhausted by default when the process 78 | * exits IF the exit is not the result of a PHP E_ERROR error. 79 | * 80 | * You can disable running the automatic shutdown of the queue by calling 81 | * this function. If you disable the task queue shutdown process, then you 82 | * MUST either run the task queue (as a result of running your event loop 83 | * or manually using the run() method) or wait on each outstanding promise. 84 | * 85 | * Note: This shutdown will occur before any destructors are triggered. 86 | * 87 | * @return void 88 | */ 89 | public function disableShutdown() 90 | { 91 | $this->enableShutdown = false; 92 | } 93 | 94 | /** 95 | * Shutdown function 96 | * 97 | * @return void 98 | * 99 | * @internal 100 | */ 101 | public function onShutdown() 102 | { 103 | if ($this->enableShutdown === false) { 104 | return; 105 | } 106 | // Only run the tasks if an E_ERROR didn't occur. 107 | $err = \error_get_last(); 108 | if (!$err || ($err['type'] ^ E_ERROR)) { 109 | $this->run(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/PubSub/Event.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since v3.0 11 | * @link http://www.github.com/bkdotcom/PubSub 12 | */ 13 | 14 | namespace bdk\PubSub; 15 | 16 | /** 17 | * Represents a basic event 18 | * 19 | * Events are passed to event subscribers/listeners 20 | * 21 | * @template Subject of mixed 22 | * @template TKey of array-key 23 | * @template TValue of mixed 24 | * 25 | * @template-extends ValueStore 26 | */ 27 | class Event extends ValueStore 28 | { 29 | /** 30 | * @var Subject Event subject - usually object or callable 31 | */ 32 | protected $subject = null; 33 | 34 | /** 35 | * @var bool Whether event subscribers should be called 36 | */ 37 | private $propagationStopped = false; 38 | 39 | /** 40 | * Construct an event with optional subject and values 41 | * 42 | * @param Subject $subject The subject of the event (usually an object) 43 | * @param array $values Values to store in the event 44 | */ 45 | public function __construct($subject = null, array $values = array()) 46 | { 47 | $this->subject = $subject; 48 | $this->setValues($values); 49 | } 50 | 51 | /** 52 | * Magic Method 53 | * 54 | * @return array{ 55 | * propagationStopped: bool, 56 | * subject: class-string|mixed, 57 | * values: array, 58 | * } 59 | */ 60 | public function __debugInfo() 61 | { 62 | return array( 63 | 'propagationStopped' => $this->propagationStopped, 64 | 'subject' => \is_object($this->subject) 65 | ? \get_class($this->subject) 66 | : $this->subject, 67 | 'values' => $this->values, 68 | ); 69 | } 70 | 71 | /** 72 | * Get Event's "subject" 73 | * 74 | * @return Subject Usually the object that published the event 75 | */ 76 | public function getSubject() 77 | { 78 | return $this->subject; 79 | } 80 | 81 | /** 82 | * Has propagation been stopped? 83 | * 84 | * If stopped, no further event subscribers will be called 85 | * 86 | * @see Event::stopPropagation() 87 | * 88 | * @return bool Whether propagation is stopped for this event 89 | */ 90 | public function isPropagationStopped() 91 | { 92 | return $this->propagationStopped; 93 | } 94 | 95 | /** 96 | * Stops the propagation of the event 97 | * 98 | * No further event subscribers will be called 99 | * 100 | * @return void 101 | */ 102 | public function stopPropagation() 103 | { 104 | $this->propagationStopped = true; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/PubSub/SubscriberInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://opensource.org/licenses/MIT MIT 9 | * @copyright 2014-2025 Brad Kent 10 | * @since v2.4 11 | * @link http://www.github.com/bkdotcom/PubSub 12 | */ 13 | 14 | namespace bdk\PubSub; 15 | 16 | /** 17 | * Provide event subscribers 18 | */ 19 | interface SubscriberInterface 20 | { 21 | /** 22 | * Return event subscribers 23 | * 24 | * The array keys are event names and the value can be: 25 | * 26 | * _method_: priority defaults to 0, onlyOnce defaults to false
27 | * array: (required) _method_, (optional) `int` priority, (optional) `bool` onlyOnce
28 | * array: any combination of the above 29 | * 30 | * _method_ = string|Callable name of public method or `Closure` 31 | * 32 | * @return array 33 | */ 34 | public function getSubscriptions(); 35 | } 36 | -------------------------------------------------------------------------------- /src/Slack/AbstractSlack.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Slack; 11 | 12 | use bdk\CurlHttpMessage\Client; 13 | use Psr\Http\Message\ResponseInterface; 14 | 15 | /** 16 | * Base class for SlackApi & SlackWebhook 17 | */ 18 | abstract class AbstractSlack 19 | { 20 | /** @var Client */ 21 | protected $client; 22 | 23 | /** @var ResponseInterface|null */ 24 | protected $lastResponse; 25 | 26 | /** 27 | * Constructor 28 | */ 29 | public function __construct() 30 | { 31 | $this->client = new Client(); 32 | } 33 | 34 | /** 35 | * @return Client 36 | */ 37 | public function getClient() 38 | { 39 | return $this->client; 40 | } 41 | 42 | /** 43 | * @return ResponseInterface|null 44 | */ 45 | public function getLastResponse() 46 | { 47 | return $this->lastResponse; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Slack/SlackWebhook.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Slack; 11 | 12 | use BadMethodCallException; 13 | use bdk\Slack\AbstractSlack; 14 | use bdk\Slack\SlackMessage; 15 | 16 | /** 17 | * Send slack message notifications using Webhooks url 18 | * 19 | * @link https://api.slack.com/incoming-webhooks 20 | * 21 | * @psalm-api 22 | */ 23 | class SlackWebhook extends AbstractSlack 24 | { 25 | /** 26 | * @var array{ 27 | * webhookUrl: string, 28 | * } 29 | */ 30 | protected $cfg = array( 31 | 'webhookUrl' => '', 32 | ); 33 | 34 | /** 35 | * Constructor 36 | * 37 | * @param string|null $webhookUrl Slack webhook url 38 | * 39 | * @throws BadMethodCallException 40 | */ 41 | public function __construct($webhookUrl = null) 42 | { 43 | $webhookUrl = $webhookUrl ?: \getenv('SLACK_WEBHOOK_URL'); 44 | if (\is_string($webhookUrl) === false) { 45 | throw new BadMethodCallException('webhookUrl must be provided.'); 46 | } 47 | $this->cfg['webhookUrl'] = $webhookUrl; 48 | parent::__construct(); 49 | } 50 | 51 | /** 52 | * POST SlackMessage to slack webhookUrl 53 | * 54 | * @param SlackMessage $slackMessage SlackMessage instance 55 | * 56 | * @return array|false 57 | */ 58 | public function post(SlackMessage $slackMessage) 59 | { 60 | $this->lastResponse = $this->client->post( 61 | $this->cfg['webhookUrl'], 62 | array( 63 | 'Content-Type' => 'application/json; charset=utf-8', 64 | ), 65 | $slackMessage 66 | ); 67 | $body = (string) $this->lastResponse->getBody(); 68 | /** @psalm-var array|false */ 69 | return \json_decode($body, true); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Teams/AbstractExtendableItem.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams; 11 | 12 | /** 13 | * The base object off of which Elements and Actions are built 14 | */ 15 | class AbstractExtendableItem extends AbstractItem 16 | { 17 | /** 18 | * Constructor 19 | * 20 | * @param array $fields Field values 21 | * @param string $type Item type 22 | */ 23 | public function __construct(array $fields, $type) 24 | { 25 | $fields = \array_merge(array( 26 | 'requires' => array(), 27 | ), $fields); 28 | parent::__construct($fields, $type); 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function getContent($version) 35 | { 36 | /** @var array */ 37 | $attrVersions = array( 38 | 'requires' => 1.2, 39 | ); 40 | 41 | $content = parent::getContent($version); 42 | foreach ($attrVersions as $name => $ver) { 43 | if ($version >= $ver) { 44 | /** @var mixed */ 45 | $content[$name] = $this->fields[$name]; 46 | } 47 | } 48 | 49 | return self::normalizeContent($content, $version); 50 | } 51 | 52 | /** 53 | * A series of key/value pairs indicating features that 54 | * the item requires with corresponding minimum version. 55 | * When a feature is missing or of insufficient version, fallback is triggered 56 | * 57 | * @param array $requires Required features 58 | * 59 | * @return static 60 | */ 61 | public function withRequires(array $requires = array()) 62 | { 63 | return $this->with('requires', $requires); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Teams/Actions/ActionInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Actions; 11 | 12 | use bdk\Teams\ItemInterface; 13 | 14 | /** 15 | * Action interface 16 | */ 17 | interface ActionInterface extends ItemInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Teams/Actions/OpenUrl.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Actions; 11 | 12 | use Psr\Http\Message\UriInterface; 13 | use RuntimeException; 14 | 15 | /** 16 | * OpenUrl action 17 | * 18 | * @see https://adaptivecards.io/explorer/Action.OpenUrl.html 19 | */ 20 | class OpenUrl extends AbstractAction 21 | { 22 | /** 23 | * Constructor 24 | * 25 | * @param string|UriInterface $url The url to open 26 | */ 27 | public function __construct($url = null) 28 | { 29 | if ($url !== null) { 30 | self::assertUrl($url); 31 | } 32 | parent::__construct(array( 33 | 'url' => $url ? (string) $url : null, 34 | ), 'Action.OpenUrl'); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function getContent($version) 41 | { 42 | if ($this->fields['url'] === null) { 43 | throw new RuntimeException('OpenUrl url is required'); 44 | } 45 | $content = parent::getContent($version); 46 | /** @var string */ 47 | $content['url'] = $this->fields['url']; 48 | return $content; 49 | } 50 | 51 | /** 52 | * Sets url 53 | * 54 | * @param string|UriInterface $url The url to open 55 | * 56 | * @return OpenUrl 57 | */ 58 | public function withUrl($url) 59 | { 60 | self::assertUrl($url); 61 | return $this->with('url', (string) $url); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Teams/Actions/ShowCard.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Actions; 11 | 12 | use bdk\Teams\Cards\AdaptiveCard; 13 | use bdk\Teams\Elements\ElementInterface; 14 | 15 | /** 16 | * ShowCard action 17 | */ 18 | class ShowCard extends AbstractAction 19 | { 20 | /** 21 | * Constructor 22 | */ 23 | public function __construct() 24 | { 25 | parent::__construct(array( 26 | 'card' => new AdaptiveCard(), 27 | ), 'Action.ShowCard'); 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | public function getContent($version) 34 | { 35 | $content = parent::getContent($version); 36 | $content['card'] = $this->getCard(); 37 | return self::normalizeContent($content, $version); 38 | } 39 | 40 | /** 41 | * Shortcut for adding element to card 42 | * 43 | * @param ElementInterface $element Card element 44 | * 45 | * @return static 46 | */ 47 | public function withAddedElement(ElementInterface $element) 48 | { 49 | return $this->with('card', $this->getCard()->withAddedElement($element)); 50 | } 51 | 52 | /** 53 | * Shortcut for adding action to card 54 | * 55 | * @param ActionInterface $action Card action 56 | * 57 | * @return static 58 | */ 59 | public function withAddedAction(ActionInterface $action) 60 | { 61 | return $this->with('card', $this->getCard()->withAddedAction($action)); 62 | } 63 | 64 | /** 65 | * Return new instance with specified card 66 | * 67 | * @param AdaptiveCard|null $card AdaptiveCard 68 | * 69 | * @return static 70 | */ 71 | public function withCard($card = null) 72 | { 73 | self::assertType($card, 'bdk\Teams\Cards\AdaptiveCard'); 74 | 75 | return $this->with('card', $card); 76 | } 77 | 78 | /** 79 | * Get the underlying card 80 | * 81 | * @return AdaptiveCard 82 | */ 83 | protected function getCard() 84 | { 85 | /** @var AdaptiveCard */ 86 | return $this->fields['card']; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Teams/Cards/AbstractCard.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Cards; 11 | 12 | use bdk\Teams\AbstractItem; 13 | 14 | /** 15 | * Abstract card 16 | */ 17 | abstract class AbstractCard extends AbstractItem implements CardInterface 18 | { 19 | /** 20 | * Returns card data 21 | * 22 | * @return array 23 | */ 24 | abstract public function getMessage(); 25 | 26 | /** 27 | * Specify data which should be serialized to JSON 28 | * 29 | * @return array 30 | */ 31 | #[\ReturnTypeWillChange] 32 | public function jsonSerialize() 33 | { 34 | return $this->getMessage(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Teams/Cards/CardInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Cards; 11 | 12 | use JsonSerializable; 13 | 14 | /** 15 | * Card interface 16 | */ 17 | interface CardInterface extends JsonSerializable 18 | { 19 | /** 20 | * Returns message card array 21 | * 22 | * @return array 23 | */ 24 | public function getMessage(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Teams/Cards/HeroCard.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Cards; 11 | 12 | use bdk\Teams\Actions\ActionInterface; 13 | use Psr\Http\Message\UriInterface; 14 | 15 | /** 16 | * Hero card 17 | * 18 | * @see https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-reference#hero-card 19 | */ 20 | class HeroCard extends AbstractCard 21 | { 22 | /** 23 | * Constructor 24 | */ 25 | public function __construct() 26 | { 27 | parent::__construct(array( 28 | 'buttons' => array(), 29 | 'images' => array(), 30 | 'subtitle' => null, 31 | 'tap' => null, 32 | 'text' => null, 33 | 'title' => null, 34 | ), 'HeroCard'); 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function getMessage() 41 | { 42 | // @phpcs:disable SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder 43 | return array( 44 | 'type' => 'message', 45 | 'attachments' => array( 46 | 'contentType' => 'application/vnd.microsoft.card.hero', 47 | 'content' => self::normalizeContent($this->fields), 48 | ), 49 | ); 50 | // @phpcs:enable 51 | } 52 | 53 | /** 54 | * Adds single button to card 55 | * 56 | * Set of actions applicable to the current card. Maximum 6 57 | * 58 | * @param string $type Button type 59 | * @param string $title Button title 60 | * @param string $value Button value 61 | * 62 | * @return static 63 | * 64 | * @see https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-actions?tabs=json 65 | */ 66 | public function withAddedButton($type, $title, $value) 67 | { 68 | self::assertEnumValue($type, 'ACTION_TYPE_', 'type'); 69 | // @phpcs:ignore SlevomatCodingStandard.Arrays.AlphabeticallySortedByKeys.IncorrectKeyOrder 70 | return $this->withAdded('buttons', array( 71 | 'type' => $type, 72 | 'title' => $title, 73 | 'value' => $value, 74 | )); 75 | } 76 | 77 | /** 78 | * Adds single image to card 79 | * 80 | * Image(s) displayed at top of card. Aspect ratio 16:9. 81 | * Currently only the first image of the array will be shown in teams 82 | * 83 | * @param string|UriInterface $url Image url 84 | * 85 | * @return static 86 | */ 87 | public function withAddedImage($url) 88 | { 89 | self::assertUrl($url); 90 | return $this->withAdded('images', array( 91 | 'url' => (string) $url, 92 | )); 93 | } 94 | 95 | /** 96 | * Sets card tap 97 | * 98 | * @param ActionInterface $action Action to take when clicking on card 99 | * 100 | * @return static 101 | */ 102 | public function withTap(ActionInterface $action) 103 | { 104 | return $this->with('tap', $action); 105 | } 106 | 107 | /** 108 | * Sets card text 109 | * 110 | * @param string $text Card text 111 | * 112 | * @return static 113 | */ 114 | public function withText($text) 115 | { 116 | $text = self::asString($text, true, __METHOD__); 117 | return $this->with('text', $text); 118 | } 119 | 120 | /** 121 | * Sets card title 122 | * 123 | * @param string $title Card title 124 | * 125 | * @return static 126 | */ 127 | public function withTitle($title) 128 | { 129 | $title = self::asString($title, true, __METHOD__); 130 | return $this->with('title', $title); 131 | } 132 | 133 | /** 134 | * Sets card subtitle 135 | * 136 | * @param string|null $subtitle Card subtitle 137 | * 138 | * @return static 139 | */ 140 | public function withSubtitle($subtitle) 141 | { 142 | $subtitle = self::asString($subtitle, true, __METHOD__); 143 | return $this->with('subtitle', $subtitle); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Teams/Elements/AbstractElement.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\Enums; 13 | 14 | /** 15 | * aka Extendable.Element 16 | */ 17 | abstract class AbstractElement extends AbstractToggleableItem implements ElementInterface 18 | { 19 | /** 20 | * Constructor 21 | * 22 | * @param array $fields Field values 23 | * @param string $type Item type 24 | */ 25 | public function __construct(array $fields, $type) 26 | { 27 | $fields = \array_merge(array( 28 | 'fallback' => null, 29 | 'height' => null, 30 | 'separator' => null, 31 | 'spacing' => null, 32 | ), $fields); 33 | parent::__construct($fields, $type); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | public function getContent($version) 40 | { 41 | $attrVersions = array( 42 | 'fallback' => 1.1, 43 | 'height' => 1.1, 44 | 'separator' => 1.0, 45 | 'spacing' => 1.0, 46 | ); 47 | 48 | $content = parent::getContent($version); 49 | foreach ($attrVersions as $name => $ver) { 50 | if ($version >= $ver) { 51 | /** @var mixed */ 52 | $content[$name] = $this->fields[$name]; 53 | } 54 | } 55 | 56 | return self::normalizeContent($content, $version); 57 | } 58 | 59 | /** 60 | * Describes what to do when an unknown element is encountered 61 | * or the requires of this or any children can't be met. 62 | * 63 | * @param ElementInterface|Enums::FALLBACK_* $fallback How to we fallback? 64 | * 65 | * @return static 66 | */ 67 | public function withFallback($fallback) 68 | { 69 | self::assertFallback( 70 | $fallback, 71 | 'bdk\\Teams\\Elements\\ElementInterface', 72 | $this->type . ' fallback should be instance of ElementInterface or one of Enum::FALLBACK_x values' 73 | ); 74 | return $this->with('fallback', $fallback); 75 | } 76 | 77 | /** 78 | * Return new instance with specified height 79 | * 80 | * @param Enums::HEIGHT_* $height Height of the element. 81 | * 82 | * @return static 83 | */ 84 | public function withHeight($height) 85 | { 86 | self::assertEnumValue($height, 'HEIGHT_', 'height'); 87 | return $this->with('height', $height); 88 | } 89 | 90 | /** 91 | * Return new instance with specified separator value 92 | * When true, draw a separating line at the top of the element. 93 | * 94 | * @param bool $separator Add separating line? 95 | * 96 | * @return static 97 | */ 98 | public function withSeparator($separator = true) 99 | { 100 | self::assertBool($separator, 'separator'); 101 | return $this->with('separator', $separator); 102 | } 103 | 104 | /** 105 | * Return new instance with specified spacing value 106 | * Controls the amount of spacing between this element and the preceding element. 107 | * 108 | * @param Enums::SPACING_* $spacing Spacing 109 | * 110 | * @return static 111 | */ 112 | public function withSpacing($spacing) 113 | { 114 | self::assertEnumValue($spacing, 'SPACING_', 'spacing'); 115 | return $this->with('spacing', $spacing); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Teams/Elements/AbstractToggleableItem.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\AbstractExtendableItem; 13 | 14 | /** 15 | * The base object off of which "toggleable" items are built 16 | */ 17 | class AbstractToggleableItem extends AbstractExtendableItem 18 | { 19 | /** 20 | * Constructor 21 | * 22 | * @param array $fields Field values 23 | * @param string $type Item type 24 | */ 25 | public function __construct(array $fields, $type) 26 | { 27 | $fields = \array_merge($this->fields, array( 28 | 'id' => null, 29 | 'isVisible' => null, 30 | ), $fields); 31 | parent::__construct($fields, $type); 32 | } 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | public function getContent($version) 38 | { 39 | $attrVersions = array( 40 | 'id' => 1.0, 41 | 'isVisible' => 1.2, 42 | ); 43 | 44 | $content = parent::getContent($version); 45 | foreach ($attrVersions as $name => $ver) { 46 | if ($version >= $ver) { 47 | /** @var mixed */ 48 | $content[$name] = $this->fields[$name]; 49 | } 50 | } 51 | 52 | return self::normalizeContent($content, $version); 53 | } 54 | 55 | /** 56 | * Return new instance with specified id value 57 | * 58 | * @param string $id A unique identifier associated with the item. 59 | * 60 | * @return static 61 | */ 62 | public function withId($id) 63 | { 64 | return $this->with('id', $id); 65 | } 66 | 67 | /** 68 | * Sets isVisible flag 69 | * If false, this item will be removed from the visual tree. 70 | * 71 | * @param bool $isVisible Visible? 72 | * 73 | * @return static 74 | */ 75 | public function withIsVisible($isVisible = true) 76 | { 77 | self::assertBool($isVisible, 'isVisible'); 78 | return $this->with('isVisible', $isVisible); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Teams/Elements/ActionSet.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\Actions\ActionInterface; 13 | use InvalidArgumentException; 14 | 15 | /** 16 | * Displays a set of actions. 17 | */ 18 | class ActionSet extends AbstractElement 19 | { 20 | /** 21 | * Constructor 22 | * 23 | * @param ActionInterface[] $actions The array of Action elements to show. 24 | */ 25 | public function __construct(array $actions = array()) 26 | { 27 | self::assertActions($actions); 28 | parent::__construct(array( 29 | 'actions' => $actions, 30 | ), 'ActionSet'); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | public function getContent($version) 37 | { 38 | $element = parent::getContent($version); 39 | /** @var ActionInterface[] */ 40 | $element['actions'] = $this->fields['actions']; 41 | return self::normalizeContent($element, $version); 42 | } 43 | 44 | /** 45 | * Return new instance with specified actions 46 | * 47 | * @param ActionInterface[] $actions The array of Action elements to show. 48 | * 49 | * @return static 50 | * 51 | * @throws InvalidArgumentException 52 | */ 53 | public function withActions(array $actions) 54 | { 55 | if ($actions === array()) { 56 | throw new InvalidArgumentException(\sprintf( 57 | '%s - Actions must be non-empty', 58 | __METHOD__ 59 | )); 60 | } 61 | self::assertActions($actions); 62 | return $this->with('actions', $actions); 63 | } 64 | 65 | /** 66 | * Return new instance with action appended 67 | * 68 | * @param ActionInterface $action The action to append 69 | * 70 | * @return static 71 | */ 72 | public function withAddedAction(ActionInterface $action) 73 | { 74 | return $this->withAdded('actions', $action); 75 | } 76 | 77 | /** 78 | * Assert that array consists of Actions 79 | * 80 | * @param ActionInterface[] $actions List to test 81 | * 82 | * @return void 83 | * 84 | * @throws InvalidArgumentException 85 | */ 86 | private static function assertActions($actions) 87 | { 88 | foreach ($actions as $i => $action) { 89 | if ($action instanceof ActionInterface) { 90 | continue; 91 | } 92 | throw new InvalidArgumentException(\sprintf( 93 | 'Invalid action found at index %s', 94 | $i 95 | )); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Teams/Elements/CommonTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\Enums; 13 | use InvalidArgumentException; 14 | use Psr\Http\Message\UriInterface; 15 | 16 | /** 17 | * Common element methods 18 | */ 19 | trait CommonTrait 20 | { 21 | /** 22 | * Return new instance with given backgroundImage 23 | * 24 | * @param string|UriInterface $url Image url 25 | * @param Enums::FILLMODE_* $fillmode fill mode 26 | * @param Enums::HORIZONTAL_ALIGNMENT_* $horizontalAlignment horizontal alignment 27 | * @param Enums::VERTICAL_ALIGNMENT_* $verticalAlignment Vertical alignment 28 | * 29 | * @return static 30 | * 31 | * @throws InvalidArgumentException 32 | */ 33 | public function withBackgroundImage($url, $fillmode = null, $horizontalAlignment = null, $verticalAlignment = null) 34 | { 35 | if ($url !== null) { 36 | self::assertUrl($url); 37 | } 38 | self::assertEnumValue($fillmode, 'FILLMODE_', 'fillmode'); 39 | self::assertEnumValue($horizontalAlignment, 'HORIZONTAL_ALIGNMENT_', 'horizontalAlignment'); 40 | self::assertEnumValue($verticalAlignment, 'VERTICAL_ALIGNMENT_', 'verticalAlignment'); 41 | $backgroundImage = self::normalizeContent(array( 42 | 'fillmode' => $fillmode, 43 | 'horizontalAlignment' => $horizontalAlignment, 44 | 'url' => $url ? (string) $url : null, 45 | 'verticalContentAlignment' => $verticalAlignment, 46 | )); 47 | return \count($backgroundImage) > 1 48 | ? $this->with('backgroundImage', $backgroundImage) 49 | : $this->with('backgroundImage', $url); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Teams/Elements/ElementInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\ItemInterface; 13 | 14 | /** 15 | * Element interface 16 | */ 17 | interface ElementInterface extends ItemInterface 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Teams/Elements/Fact.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\AbstractItem; 13 | 14 | /** 15 | * Fact 16 | * 17 | * @see https://adaptivecards.io/explorer/Fact.html 18 | */ 19 | class Fact extends AbstractItem 20 | { 21 | /** 22 | * Constructor 23 | * 24 | * @param string|int $title Fact title 25 | * @param string|numeric $value Fact value 26 | */ 27 | public function __construct($title, $value) 28 | { 29 | parent::__construct(array( 30 | 'title' => self::asString($title, false, __METHOD__), 31 | 'value' => self::asString($value, false, __METHOD__), 32 | ), 'Fact'); 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | */ 38 | public function getContent($version) 39 | { 40 | return array( 41 | 'title' => $this->fields['title'], 42 | 'value' => $this->fields['value'], 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Teams/Elements/FactSet.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use InvalidArgumentException; 13 | 14 | /** 15 | * FactSet card element 16 | */ 17 | class FactSet extends AbstractElement 18 | { 19 | /** 20 | * Constructor 21 | * 22 | * @param Fact[]|array[] $facts Facts 23 | */ 24 | public function __construct(array $facts = array()) 25 | { 26 | parent::__construct(array( 27 | 'facts' => self::asFacts($facts), 28 | ), 'FactSet'); 29 | } 30 | 31 | /** 32 | * {@inheritDoc} 33 | */ 34 | public function getContent($version) 35 | { 36 | $content = parent::getContent($version); 37 | /** @var Fact[] */ 38 | $content['facts'] = $this->fields['facts']; 39 | return self::normalizeContent($content, $version); 40 | } 41 | 42 | /** 43 | * Adds fact to element 44 | * 45 | * @param Fact $fact Fact 46 | * 47 | * @return static 48 | */ 49 | public function withAddedFact(Fact $fact) 50 | { 51 | return $this->withAdded('facts', $fact); 52 | } 53 | 54 | /** 55 | * Return new instance with provided facts 56 | * 57 | * @param array $facts Fact objects of key/value array 58 | * 59 | * @return static 60 | * 61 | * @throws InvalidArgumentException 62 | */ 63 | public function withFacts(array $facts) 64 | { 65 | if ($facts === array()) { 66 | throw new InvalidArgumentException(\sprintf( 67 | '%s - Facts must be non-empty', 68 | __METHOD__ 69 | )); 70 | } 71 | return $this->with('facts', self::asFacts($facts)); 72 | } 73 | 74 | /** 75 | * "Normalize" facts / name/values to Facts 76 | * 77 | * @param array $facts Key/value array or list of Fact objects 78 | * 79 | * @return Fact[] 80 | * 81 | * @throws InvalidArgumentException 82 | */ 83 | private function asFacts(array $facts) 84 | { 85 | $factsNew = array(); 86 | \array_walk( 87 | $facts, 88 | /** 89 | * @param mixed $value 90 | * @param array-key $key 91 | */ 92 | static function ($value, $key) use (&$factsNew) { 93 | if ($value instanceof Fact) { 94 | $factsNew[] = $value; 95 | return; 96 | } 97 | if (\is_string($value) || \is_numeric($value)) { 98 | $factsNew[] = new Fact($key, $value); 99 | return; 100 | } 101 | throw new InvalidArgumentException(\sprintf( 102 | 'Invalid Fact or value encountered at %s. Expected Fact, string, or numeric. %s provided.', 103 | $key, 104 | self::getDebugType($value) 105 | )); 106 | } 107 | ); 108 | return $factsNew; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Teams/Elements/MediaSource.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams\Elements; 11 | 12 | use bdk\Teams\AbstractItem; 13 | use Psr\Http\Message\UriInterface; 14 | use RuntimeException; 15 | 16 | /** 17 | * MediaSource element 18 | * 19 | * CURRENTLY NOT SUPPORTED BY TEAMS 20 | * 21 | * @see https://adaptivecards.io/explorer/MediaSource.html 22 | */ 23 | class MediaSource extends AbstractItem 24 | { 25 | /** 26 | * Constructor 27 | * 28 | * @param string|UriInterface $url URL 29 | * @param string $mimeType Mime type 30 | */ 31 | public function __construct($url = null, $mimeType = null) 32 | { 33 | if ($url !== null) { 34 | self::assertUrl($url, true); 35 | } 36 | parent::__construct(array( 37 | 'mimeType' => $mimeType, 38 | 'url' => $url ? (string) $url : null, 39 | ), 'MediaSource'); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function getContent($version) 46 | { 47 | if ($this->fields['mimeType'] === null) { 48 | throw new RuntimeException('MediaSource mimeType is required'); 49 | } 50 | if ($this->fields['url'] === null) { 51 | throw new RuntimeException('MediaSource url is required'); 52 | } 53 | return array( 54 | 'mimeType' => $this->fields['mimeType'], 55 | 'url' => $this->fields['url'], 56 | ); 57 | } 58 | 59 | /** 60 | * Returns new instance with specified mime-type 61 | * Mime type of associated media (e.g. "video/mp4") 62 | * 63 | * @param string $mimeType Mime-type 64 | * 65 | * @return static 66 | */ 67 | public function withMimeType($mimeType) 68 | { 69 | $mimeType = self::asString($mimeType, false, __METHOD__); 70 | return $this->with('mimeType', $mimeType); 71 | } 72 | 73 | /** 74 | * Returns new instance with specified url 75 | * URL to media. 76 | * Supports data URI in version 1.2+ 77 | * 78 | * @param string|UriInterface $url Url 79 | * 80 | * @return static 81 | */ 82 | public function withUrl($url) 83 | { 84 | self::assertUrl($url, true); 85 | return $this->with('url', (string) $url); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Teams/Enums.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams; 11 | 12 | /** 13 | * Adaptive card enum values 14 | */ 15 | class Enums 16 | { 17 | const ACTION_MODE_PRIMARY = 'primary'; 18 | const ACTION_MODE_SECONDARY = 'secondary'; 19 | 20 | // action style 21 | const ACTION_STYLE_DEFAULT = 'default'; 22 | const ACTION_STYLE_DESTRUCTIVE = 'destructive'; 23 | const ACTION_STYLE_POSITIVE = 'positive'; 24 | 25 | const ACTION_TYPE_IMBACK = 'imBack'; 26 | const ACTION_TYPE_INVOKE = 'invoke'; 27 | const ACTION_TYPE_MESSAGEBACK = 'messageBack'; 28 | const ACTION_TYPE_OPENURL = 'openUrl'; 29 | const ACTION_TYPE_SIGNIN = 'signin'; 30 | 31 | // block element height 32 | const HEIGHT_AUTO = 'auto'; 33 | const HEIGHT_STRETCH = 'stretch'; 34 | 35 | // colors 36 | const COLOR_ACCENT = 'accent'; 37 | const COLOR_ATTENTION = 'attention'; 38 | const COLOR_DARK = 'dark'; 39 | const COLOR_DEFAULT = 'default'; 40 | const COLOR_GOOD = 'good'; 41 | const COLOR_LIGHT = 'light'; 42 | const COLOR_WARNING = 'warning'; 43 | 44 | // column width 45 | const COLUMN_WIDTH_AUTO = 'auto'; 46 | const COLUMN_WIDTH_STRETCH = 'stretch'; 47 | 48 | const CONTAINER_STYLE_ACCENT = 'accent'; 49 | const CONTAINER_STYLE_ATTENTION = 'attention'; // Added in version 1.2. 50 | const CONTAINER_STYLE_DEFAULT = 'default'; 51 | const CONTAINER_STYLE_EMPHASIS = 'emphasis'; 52 | const CONTAINER_STYLE_GOOD = 'good'; // Added in version 1.2. 53 | const CONTAINER_STYLE_WARNING = 'warning'; // Added in version 1.2. 54 | 55 | const FALLBACK_DROP = 'drop'; 56 | 57 | // fillmode (used for background image) 58 | const FILLMODE_COVER = 'cover'; 59 | const FILLMODE_REPEAT = 'repeat'; 60 | const FILLMODE_REPEAT_X = 'repeatHorizontally'; 61 | const FILLMODE_REPEAT_Y = 'repeatVertically'; 62 | 63 | // font size 64 | const FONT_SIZE_DEFAULT = 'default'; 65 | const FONT_SIZE_EXTRA_LARGE = 'extraLarge'; 66 | const FONT_SIZE_LARGE = 'large'; 67 | const FONT_SIZE_MEDIUM = 'medium'; 68 | const FONT_SIZE_SMALL = 'small'; 69 | 70 | // font type 71 | const FONT_TYPE_DEFAULT = 'default'; 72 | const FONT_TYPE_MONOSPACE = 'monospace'; 73 | 74 | // font weight 75 | const FONT_WEIGHT_BOLDER = 'bolder'; 76 | const FONT_WEIGHT_DEFAULT = 'default'; 77 | const FONT_WEIGHT_LIGHTER = 'lighter'; 78 | 79 | // horizontal alignment 80 | const HORIZONTAL_ALIGNMENT_CENTER = 'center'; 81 | const HORIZONTAL_ALIGNMENT_LEFT = 'left'; 82 | const HORIZONTAL_ALIGNMENT_RIGHT = 'right'; 83 | 84 | // image size 85 | const IMAGE_SIZE_AUTO = 'auto'; 86 | const IMAGE_SIZE_LARGE = 'large'; 87 | const IMAGE_SIZE_MEDIUM = 'medium'; 88 | const IMAGE_SIZE_SMALL = 'small'; 89 | const IMAGE_SIZE_STRETCH = 'stretch'; 90 | 91 | // image style 92 | const IMAGE_STYLE_DEFAULT = 'default'; 93 | const IMAGE_STYLE_PERSON = 'person'; 94 | 95 | // spacing 96 | const SPACING_DEFAULT = 'default'; 97 | const SPACING_EXTRA_LARGE = 'extraLarge'; 98 | const SPACING_LARGE = 'large'; 99 | const SPACING_MEDIUM = 'medium'; 100 | const SPACING_NONE = 'none'; 101 | const SPACING_PADDING = 'padding'; 102 | const SPACING_SMALL = 'small'; 103 | 104 | const TEXTBLOCK_STYLE_DEFAULT = 'default'; 105 | const TEXTBLOCK_STYLE_HEADING = 'heading'; 106 | 107 | // horizontal alignment 108 | const VERTICAL_ALIGNMENT_TOP = 'top'; 109 | const VERTICAL_ALIGNMENT_CENTER = 'center'; 110 | const VERTICAL_ALIGNMENT_BOTTOM = 'bottom'; 111 | } 112 | -------------------------------------------------------------------------------- /src/Teams/ItemInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams; 11 | 12 | use OutOfBoundsException; 13 | 14 | /** 15 | * Item interface 16 | */ 17 | interface ItemInterface 18 | { 19 | /** 20 | * Get attribute value 21 | * 22 | * @param string $name Field name 23 | * 24 | * @return mixed 25 | * 26 | * @throws OutOfBoundsException 27 | */ 28 | public function get($name); 29 | 30 | /** 31 | * Get element content 32 | * 33 | * @param float|null $version Card version 34 | * 35 | * @return array 36 | */ 37 | public function getContent($version); 38 | } 39 | -------------------------------------------------------------------------------- /src/Teams/TeamsWebhook.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT MIT 7 | * @copyright 2023-2025 Brad Kent 8 | */ 9 | 10 | namespace bdk\Teams; 11 | 12 | use BadMethodCallException; 13 | use bdk\CurlHttpMessage\Client; 14 | use bdk\Teams\Cards\CardInterface; 15 | use Psr\Http\Message\ResponseInterface; 16 | 17 | /** 18 | * Send teams message notifications using webhook url 19 | * 20 | * @psalm-api 21 | */ 22 | class TeamsWebhook 23 | { 24 | /** @var array{ 25 | * webhookUrl: string, 26 | * } 27 | */ 28 | protected $cfg = array( 29 | 'webhookUrl' => '', 30 | ); 31 | 32 | /** @var Client */ 33 | protected $client; 34 | 35 | /** @var ResponseInterface|null */ 36 | protected $lastResponse = null; 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @param string|null $webhookUrl Slack webhook url 42 | * 43 | * @throws BadMethodCallException 44 | */ 45 | public function __construct($webhookUrl = null) 46 | { 47 | $webhookUrl = $webhookUrl ?: \getenv('TEAMS_WEBHOOK_URL'); 48 | if (\is_string($webhookUrl) === false) { 49 | throw new BadMethodCallException('webhookUrl must be provided.'); 50 | } 51 | $this->cfg['webhookUrl'] = $webhookUrl; 52 | $this->client = new Client(); 53 | } 54 | 55 | /** 56 | * @return Client 57 | */ 58 | public function getClient() 59 | { 60 | return $this->client; 61 | } 62 | 63 | /** 64 | * @return ResponseInterface|null 65 | */ 66 | public function getLastResponse() 67 | { 68 | return $this->lastResponse; 69 | } 70 | 71 | /** 72 | * POST message / card to teams via channel webhook 73 | * 74 | * @param CardInterface $card Card instance 75 | * 76 | * @return array|false 77 | */ 78 | public function post(CardInterface $card) 79 | { 80 | $this->lastResponse = $this->client->post( 81 | $this->cfg['webhookUrl'], 82 | array( 83 | 'Content-Type' => 'application/json; charset=utf-8', 84 | ), 85 | $card 86 | ); 87 | $body = (string) $this->lastResponse->getBody(); 88 | /** @var array|false */ 89 | return \json_decode($body, true); 90 | } 91 | } 92 | --------------------------------------------------------------------------------