From 98a0d58051a310e673b6f9c625a5b450a7a6060c Mon Sep 17 00:00:00 2001 From: krishna Date: Mon, 19 May 2025 10:42:56 +0530 Subject: [PATCH 01/20] Add feature: add support for creating and updating WhatsApp message templates --- README.md | 108 ++++++++++++++++++ src/Client.php | 66 +++++++++++ .../TemplateRequest/CreateTemplateRequest.php | 57 +++++++++ .../TemplateRequest/UpdateTemplateRequest.php | 57 +++++++++ src/WhatsAppCloudApi.php | 80 +++++++++++++ 5 files changed, 368 insertions(+) create mode 100644 src/Request/TemplateRequest/CreateTemplateRequest.php create mode 100644 src/Request/TemplateRequest/UpdateTemplateRequest.php diff --git a/README.md b/README.md index d9a28d1..505dcb3 100644 --- a/README.md +++ b/README.md @@ -503,6 +503,112 @@ $whatsapp_cloud_api->updateBusinessProfile([ ]); ``` +### Create a Template +```php + 'your-configured-from-phone-number-id', + 'access_token' => 'your-facebook-whatsapp-application-token', + 'business_id' => 'your-business-id', +]); + +$whatsapp->createTemplate( + 'seasonal_promotion', + 'MARKETING', // UTILITY | MARKETING | AUTHENTICATION + 'en_US', + [ + [ + 'type' => 'HEADER', + 'format' => 'TEXT', + 'text' => 'Our {{1}} is on!', + 'example' => [ + 'header_text' => ['Summer Sale'] + ] + ], + [ + 'type' => 'BODY', + 'text' => 'Shop now through {{1}} and use code {{2}} to get {{3}} off of all merchandise.', + 'example' => [ + 'body_text' => [[ 'the end of August', '25OFF', '25%' ]] + ] + ], + [ + 'type' => 'FOOTER', + 'text' => 'Use the buttons below to manage your marketing subscriptions' + ], + [ + 'type' => 'BUTTONS', + 'buttons' => [ + [ + 'type' => 'QUICK_REPLY', + 'text' => 'Unsubscribe from Promos' + ], + [ + 'type' => 'QUICK_REPLY', + 'text' => 'Unsubscribe from All' + ] + ] + ] + ] +); +``` + +### Update a Template +```php + 'your-configured-from-phone-number-id', + 'access_token' => 'your-facebook-whatsapp-application-token', + 'business_id' => 'your-business-id', +]); + +$template_id = 'your-template-id'; + +// New payload to update the template +$payload = [ + 'category' => 'MARKETING', + 'language' => 'en_US', + 'components' => [ + [ + 'type' => 'HEADER', + 'format' => 'TEXT', + 'text' => 'Hello {{1}} is live now!', + 'example' => [ + 'header_text' => ['Flash Sale'] + ] + ], + [ + 'type' => 'BODY', + 'text' => 'Buy now before {{1}} ends. Use code {{2}} to get {{3}} off!', + 'example' => [ + 'body_text' => [['midnight', 'FLASH20', '20%']] + ] + ], + [ + 'type' => 'FOOTER', + 'text' => 'Tap below to shop.' + ], + [ + 'type' => 'BUTTONS', + 'buttons' => [ + [ + 'type' => 'QUICK_REPLY', + 'text' => 'Show Me Deals' + ], + [ + 'type' => 'QUICK_REPLY', + 'text' => 'Stop Promotions' + ] + ] + ] + ] +]; + +$whatsapp->updateTemplateById($template_id, $payload); +``` + Fields list: https://developers.facebook.com/docs/whatsapp/cloud-api/reference/business-profiles ## Features @@ -528,6 +634,8 @@ Fields list: https://developers.facebook.com/docs/whatsapp/cloud-api/reference/b - Get/Update Business Profile - Webhook verification - Webhook notifications +- Create Template +- Update Template ## Getting Help - Ask a question on the [Discussions forum](https://github.com/netflie/whatsapp-cloud-api/discussions "Discussions forum") diff --git a/src/Client.php b/src/Client.php index 05c2a96..9aadaee 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,6 +4,8 @@ use Netflie\WhatsAppCloudApi\Http\ClientHandler; use Netflie\WhatsAppCloudApi\Http\GuzzleClientHandler; +use Netflie\WhatsAppCloudApi\Request\TemplateRequest\CreateTemplateRequest; +use Netflie\WhatsAppCloudApi\Request\TemplateRequest\UpdateTemplateRequest; class Client { @@ -191,4 +193,68 @@ private function buildRequestUri(string $node_path): string { return $this->buildBaseUri() . '/' . $node_path; } + + /** + * Create a new WhatsApp message template using the Graph API. + * + * @param CreateTemplateRequest $request The template creation request payload. + * + * @return Response Raw response from the server. + * + * @throws \Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function createTemplate(CreateTemplateRequest $request): Response + { + $raw_response = $this->handler->postJsonData( + $this->buildRequestUri($request->nodePath()), + $request->body(), + $request->headers(), + $request->timeout() + ); + + $return_response = new Response( + $request, + $raw_response->body(), + $raw_response->httpResponseCode(), + $raw_response->headers() + ); + + if ($return_response->isError()) { + $return_response->throwException(); + } + + return $return_response; + } + + /** + * Update an existing WhatsApp message template using the Graph API. + * + * @param UpdateTemplateRequest $request The template update request payload. + * + * @return Response Raw response from the server. + * + * @throws \Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function updateTemplate(UpdateTemplateRequest $request): Response + { + $raw_response = $this->handler->postJsonData( + $this->buildRequestUri($request->nodePath()), + $request->body(), + $request->headers(), + $request->timeout() + ); + + $return_response = new Response( + $request, + $raw_response->body(), + $raw_response->httpResponseCode(), + $raw_response->headers() + ); + + if ($return_response->isError()) { + $return_response->throwException(); + } + + return $return_response; + } } diff --git a/src/Request/TemplateRequest/CreateTemplateRequest.php b/src/Request/TemplateRequest/CreateTemplateRequest.php new file mode 100644 index 0000000..1832a46 --- /dev/null +++ b/src/Request/TemplateRequest/CreateTemplateRequest.php @@ -0,0 +1,57 @@ +payload = $payload; + $this->business_id = $business_id; + + parent::__construct($access_token, $timeout); + } + + /** + * Returns the payload to be sent as the request body. + * + * @return array The request body. + */ + public function body(): array + { + return $this->payload; + } + + /** + * Returns the Graph API node path for creating the template. + * + * @return string The URI path segment (e.g., {business_id}/message_templates). + */ + public function nodePath(): string + { + return $this->business_id . '/message_templates'; + } +} diff --git a/src/Request/TemplateRequest/UpdateTemplateRequest.php b/src/Request/TemplateRequest/UpdateTemplateRequest.php new file mode 100644 index 0000000..e450494 --- /dev/null +++ b/src/Request/TemplateRequest/UpdateTemplateRequest.php @@ -0,0 +1,57 @@ +template_id = $template_id; + $this->payload = $payload; + + parent::__construct($access_token, $timeout); + } + + /** + * Returns the payload to be sent as the request body. + * + * @return array The request body. + */ + public function body(): array + { + return $this->payload; + } + + /** + * Returns the Graph API node path for updating the template. + * + * @return string The URI path segment (e.g., template ID). + */ + public function nodePath(): string + { + return $this->template_id; + } +} diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index 9ee9124..4aecd62 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -10,6 +10,7 @@ use Netflie\WhatsAppCloudApi\Message\MultiProduct\Action as MultiProductAction; use Netflie\WhatsAppCloudApi\Message\OptionsList\Action; use Netflie\WhatsAppCloudApi\Message\Template\Component; +use Netflie\WhatsAppCloudApi\Request\TemplateRequest\CreateTemplateRequest; class WhatsAppCloudApi { @@ -609,4 +610,83 @@ public function replyTo(string $message_id): self return $this; } + + /** + * Creates a new WhatsApp message template. + * + * @param string $name The name of the template. + * @param string $category The category of the template (e.g., MARKETING, TRANSACTIONAL). + * @param string $language The language code (e.g., en_US). + * @param array $components Array of template components (header, body, footer, buttons). + * + * @return Response The API response. + * + * @throws \RuntimeException If the template already exists or has conflicting parameters. + * @throws \Netflie\WhatsAppCloudApi\Response\ResponseException For other API errors. + */ + public function createTemplate(string $name, string $category, string $language, array $components): Response + { + $request = new CreateTemplateRequest( + $this->app->accessToken(), + $this->app->businessId(), + [ + 'name' => $name, + 'category' => $category, + 'language' => $language, + 'components' => $components, + ], + $this->timeout + ); + + try { + return $this->client->createTemplate($request); + } catch (\Netflie\WhatsAppCloudApi\Response\ResponseException $e) { + $decoded = json_decode($e->getMessage(), true); + + if ( + isset($decoded['error']['error_subcode']) && + $decoded['error']['error_subcode'] === 2388024 + ) { + throw new \RuntimeException('Template already exists or has conflicting parameters.'); + } + + throw $e; + } + } + + /** + * Updates an existing WhatsApp message template by ID. + * + * @param string $template_id The ID of the template to update. + * @param array $payload The update payload (components, name, etc.). + * + * @return Response The API response. + * + * @throws \RuntimeException If the template update fails due to conflict or invalid data. + * @throws \Netflie\WhatsAppCloudApi\Response\ResponseException For other API errors. + */ + public function updateTemplateById(string $template_id, array $payload): Response + { + $request = new Request\TemplateRequest\UpdateTemplateRequest( + $this->app->accessToken(), + $template_id, + $payload, + $this->timeout + ); + + try { + return $this->client->updateTemplate($request); + } catch (\Netflie\WhatsAppCloudApi\Response\ResponseException $e) { + $decoded = json_decode($e->getMessage(), true); + + if ( + isset($decoded['error']['error_subcode']) && + (int)$decoded['error']['error_subcode'] === 2388024 + ) { + throw new \RuntimeException('Template update failed due to conflict or invalid data.'); + } + + throw $e; + } + } } From c9b356092fbecb0df51880dee85450751c27b9e1 Mon Sep 17 00:00:00 2001 From: krishna Date: Fri, 30 May 2025 15:52:25 +0530 Subject: [PATCH 02/20] Refactor Client class and improve ResponseException handling - Extracted the common JSON request/response logic in Client class to a private sendJsonRequest helper method. - Replaced the usage of getMessage() with responseData() in ResponseException for better error body parsing. - Removed redundant error decoding logic in updateTemplateById and similar methods. - Updated docblocks throughout Client class for clarity and maintainability. --- src/Client.php | 91 ++++++++++++++++------------------------ src/WhatsAppCloudApi.php | 30 +------------ 2 files changed, 39 insertions(+), 82 deletions(-) diff --git a/src/Client.php b/src/Client.php index 9aadaee..7979ed2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -38,13 +38,15 @@ public function __construct(string $graph_version, ?ClientHandler $handler = nul } /** - * Send a message request to. + * Sends a JSON-encoded POST request and processes the response. * - * @return Response Raw response from the server. + * @param Request\RequestWithBody $request The request to send. * - * @throws Netflie\WhatsAppCloudApi\Response\ResponseException + * @return Response The API response. + * + * @throws Response\ResponseException If the API returns an error. */ - public function sendMessage(Request\RequestWithBody $request): Response + private function sendJsonRequest(Request\RequestWithBody $request): Response { $raw_response = $this->handler->postJsonData( $this->buildRequestUri($request->nodePath()), @@ -53,6 +55,21 @@ public function sendMessage(Request\RequestWithBody $request): Response $request->timeout() ); + return $this->processResponse($request, $raw_response); + } + + /** + * Processes a raw HTTP response and returns a Response object. + * + * @param Request\Request $request The original request. + * @param mixed $raw_response The raw HTTP response data. + * + * @return Response The processed response. + * + * @throws Response\ResponseException If the response indicates an error. + */ + private function processResponse(Request\Request $request, $raw_response): Response + { $return_response = new Response( $request, $raw_response->body(), @@ -67,6 +84,18 @@ public function sendMessage(Request\RequestWithBody $request): Response return $return_response; } + /** + * Send a message request to. + * + * @return Response Raw response from the server. + * + * @throws Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function sendMessage(Request\RequestWithBody $request): Response + { + return $this->sendJsonRequest($request); + } + /** * Upload a media file to Facebook servers. * @@ -83,18 +112,7 @@ public function uploadMedia(Request\MediaRequest\UploadMediaRequest $request): R $request->timeout() ); - $return_response = new Response( - $request, - $raw_response->body(), - $raw_response->httpResponseCode(), - $raw_response->headers() - ); - - if ($return_response->isError()) { - $return_response->throwException(); - } - - return $return_response; + return $this->processResponse($request, $raw_response); } /** @@ -205,25 +223,7 @@ private function buildRequestUri(string $node_path): string */ public function createTemplate(CreateTemplateRequest $request): Response { - $raw_response = $this->handler->postJsonData( - $this->buildRequestUri($request->nodePath()), - $request->body(), - $request->headers(), - $request->timeout() - ); - - $return_response = new Response( - $request, - $raw_response->body(), - $raw_response->httpResponseCode(), - $raw_response->headers() - ); - - if ($return_response->isError()) { - $return_response->throwException(); - } - - return $return_response; + return $this->sendJsonRequest($request); } /** @@ -237,24 +237,7 @@ public function createTemplate(CreateTemplateRequest $request): Response */ public function updateTemplate(UpdateTemplateRequest $request): Response { - $raw_response = $this->handler->postJsonData( - $this->buildRequestUri($request->nodePath()), - $request->body(), - $request->headers(), - $request->timeout() - ); - - $return_response = new Response( - $request, - $raw_response->body(), - $raw_response->httpResponseCode(), - $raw_response->headers() - ); - - if ($return_response->isError()) { - $return_response->throwException(); - } - - return $return_response; + return $this->sendJsonRequest($request); } + } diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index 4aecd62..b9b4032 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -638,20 +638,7 @@ public function createTemplate(string $name, string $category, string $language, $this->timeout ); - try { - return $this->client->createTemplate($request); - } catch (\Netflie\WhatsAppCloudApi\Response\ResponseException $e) { - $decoded = json_decode($e->getMessage(), true); - - if ( - isset($decoded['error']['error_subcode']) && - $decoded['error']['error_subcode'] === 2388024 - ) { - throw new \RuntimeException('Template already exists or has conflicting parameters.'); - } - - throw $e; - } + return $this->client->createTemplate($request); } /** @@ -674,19 +661,6 @@ public function updateTemplateById(string $template_id, array $payload): Respons $this->timeout ); - try { - return $this->client->updateTemplate($request); - } catch (\Netflie\WhatsAppCloudApi\Response\ResponseException $e) { - $decoded = json_decode($e->getMessage(), true); - - if ( - isset($decoded['error']['error_subcode']) && - (int)$decoded['error']['error_subcode'] === 2388024 - ) { - throw new \RuntimeException('Template update failed due to conflict or invalid data.'); - } - - throw $e; - } + return $this->client->updateTemplate($request); } } From d5d850ee2d7b3008593058c2f33276971c493b46 Mon Sep 17 00:00:00 2001 From: krishna Date: Fri, 30 May 2025 16:06:45 +0530 Subject: [PATCH 03/20] remove empty line before closing brace in Client class --- src/Client.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 7979ed2..85db4b2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -239,5 +239,4 @@ public function updateTemplate(UpdateTemplateRequest $request): Response { return $this->sendJsonRequest($request); } - } From 52b93f4cc94f513b63b2967923f0397c02924f0c Mon Sep 17 00:00:00 2001 From: krishna Date: Tue, 10 Jun 2025 13:08:40 +0530 Subject: [PATCH 04/20] Added PHPUnit tests for the new methods --- src/Client.php | 92 +++++++++---------- tests/Integration/WhatsAppCloudApiTest.php | 36 ++++++++ ...WhatsAppCloudApiTestConfiguration.php.dist | 2 + 3 files changed, 82 insertions(+), 48 deletions(-) diff --git a/src/Client.php b/src/Client.php index 85db4b2..667ac5b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -38,15 +38,13 @@ public function __construct(string $graph_version, ?ClientHandler $handler = nul } /** - * Sends a JSON-encoded POST request and processes the response. - * - * @param Request\RequestWithBody $request The request to send. + * Send a message request to. * - * @return Response The API response. + * @return Response Raw response from the server. * - * @throws Response\ResponseException If the API returns an error. + * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ - private function sendJsonRequest(Request\RequestWithBody $request): Response + public function sendMessage(Request\RequestWithBody $request): Response { $raw_response = $this->handler->postJsonData( $this->buildRequestUri($request->nodePath()), @@ -55,21 +53,6 @@ private function sendJsonRequest(Request\RequestWithBody $request): Response $request->timeout() ); - return $this->processResponse($request, $raw_response); - } - - /** - * Processes a raw HTTP response and returns a Response object. - * - * @param Request\Request $request The original request. - * @param mixed $raw_response The raw HTTP response data. - * - * @return Response The processed response. - * - * @throws Response\ResponseException If the response indicates an error. - */ - private function processResponse(Request\Request $request, $raw_response): Response - { $return_response = new Response( $request, $raw_response->body(), @@ -84,18 +67,6 @@ private function processResponse(Request\Request $request, $raw_response): Respo return $return_response; } - /** - * Send a message request to. - * - * @return Response Raw response from the server. - * - * @throws Netflie\WhatsAppCloudApi\Response\ResponseException - */ - public function sendMessage(Request\RequestWithBody $request): Response - { - return $this->sendJsonRequest($request); - } - /** * Upload a media file to Facebook servers. * @@ -112,7 +83,18 @@ public function uploadMedia(Request\MediaRequest\UploadMediaRequest $request): R $request->timeout() ); - return $this->processResponse($request, $raw_response); + $return_response = new Response( + $request, + $raw_response->body(), + $raw_response->httpResponseCode(), + $raw_response->headers() + ); + + if ($return_response->isError()) { + $return_response->throwException(); + } + + return $return_response; } /** @@ -213,30 +195,44 @@ private function buildRequestUri(string $node_path): string } /** - * Create a new WhatsApp message template using the Graph API. + * Handles sending a template request (create/update) and processing the response. * - * @param CreateTemplateRequest $request The template creation request payload. + * @param object $request The request object with required methods. * - * @return Response Raw response from the server. + * @return Response * * @throws \Netflie\WhatsAppCloudApi\Response\ResponseException */ + private function sendTemplateRequest($request): Response + { + $raw_response = $this->handler->postJsonData( + $this->buildRequestUri($request->nodePath()), + $request->body(), + $request->headers(), + $request->timeout() + ); + + $return_response = new Response( + $request, + $raw_response->body(), + $raw_response->httpResponseCode(), + $raw_response->headers() + ); + + if ($return_response->isError()) { + $return_response->throwException(); + } + + return $return_response; + } + public function createTemplate(CreateTemplateRequest $request): Response { - return $this->sendJsonRequest($request); + return $this->sendTemplateRequest($request); } - /** - * Update an existing WhatsApp message template using the Graph API. - * - * @param UpdateTemplateRequest $request The template update request payload. - * - * @return Response Raw response from the server. - * - * @throws \Netflie\WhatsAppCloudApi\Response\ResponseException - */ public function updateTemplate(UpdateTemplateRequest $request): Response { - return $this->sendJsonRequest($request); + return $this->sendTemplateRequest($request); } } diff --git a/tests/Integration/WhatsAppCloudApiTest.php b/tests/Integration/WhatsAppCloudApiTest.php index ea7bb1a..0a24d0d 100644 --- a/tests/Integration/WhatsAppCloudApiTest.php +++ b/tests/Integration/WhatsAppCloudApiTest.php @@ -475,4 +475,40 @@ public function test_update_business_profile() $this->assertEquals(200, $response->httpStatusCode()); $this->assertEquals(false, $response->isError()); } + + public function test_create_template() + { + $name = 'test_template_' . uniqid(); + $category = 'MARKETING'; + $language = 'en_US'; + $components = [ + [ + 'type' => 'BODY', + 'text' => 'Test body', + ], + ]; + + $response = $this->whatsapp_app_cloud_api->createTemplate($name, $category, $language, $components); + + $this->assertInstanceOf(\Netflie\WhatsAppCloudApi\Response::class, $response); + $this->assertFalse($response->isError(), $response->body()); + } + + public function test_update_template_by_id() + { + $templateId = WhatsAppCloudApiTestConfiguration::$template_id; + $payload = [ + 'components' => [ + [ + 'type' => 'BODY', + 'text' => 'Updated body', + ], + ], + ]; + + $response = $this->whatsapp_app_cloud_api->updateTemplateById($templateId, $payload); + + $this->assertInstanceOf(\Netflie\WhatsAppCloudApi\Response::class, $response); + $this->assertFalse($response->isError(), $response->body()); + } } diff --git a/tests/WhatsAppCloudApiTestConfiguration.php.dist b/tests/WhatsAppCloudApiTestConfiguration.php.dist index e91757e..abfdade 100644 --- a/tests/WhatsAppCloudApiTestConfiguration.php.dist +++ b/tests/WhatsAppCloudApiTestConfiguration.php.dist @@ -20,4 +20,6 @@ class WhatsAppCloudApiTestConfiguration { public static $catalog_id = ''; public static $product_sku_id = ''; + public static $template_id = ''; + } From a48bf24c8f61ff2ced6fa785dc3290d24b3f19b5 Mon Sep 17 00:00:00 2001 From: Rafael Queiroz Date: Tue, 9 Dec 2025 10:55:37 -0300 Subject: [PATCH 05/20] Support empty profile name on Notification --- src/WebHook/Notification/MessageNotificationFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php index 8db65cf..26432e8 100644 --- a/src/WebHook/Notification/MessageNotificationFactory.php +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -130,7 +130,7 @@ private function decorateNotification(MessageNotification $notification, array $ if ($contact) { $notification->withCustomer(new Support\Customer( $contact['wa_id'], - $contact['profile']['name'], + $contact['profile']['name'] ?? '', $message['from'] )); } From b7b8c3bee3531d37ddd9ac313bbabba784d8e4ab Mon Sep 17 00:00:00 2001 From: "Ali A. Dhillon" Date: Wed, 12 Feb 2025 16:08:52 +0500 Subject: [PATCH 06/20] Add support for frequently forwarded messages in context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have this field in the context object frequently_forwarded — Boolean. Set to true if the message received by the business has been forwarded more than 5 times. --- src/WebHook/Notification/MessageNotification.php | 9 +++++++++ src/WebHook/Notification/MessageNotificationFactory.php | 1 + src/WebHook/Notification/Support/Context.php | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/src/WebHook/Notification/MessageNotification.php b/src/WebHook/Notification/MessageNotification.php index 6919aa5..145adac 100644 --- a/src/WebHook/Notification/MessageNotification.php +++ b/src/WebHook/Notification/MessageNotification.php @@ -35,6 +35,15 @@ public function isForwarded(): bool return $this->context->isForwarded(); } + public function isFrequentlyForwarded(): bool + { + if (!$this->context) { + return false; + } + + return $this->context->isFrequentlyForwarded(); + } + public function context(): ?Support\Context { return $this->context; diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php index 8db65cf..61fdc68 100644 --- a/src/WebHook/Notification/MessageNotificationFactory.php +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -146,6 +146,7 @@ private function decorateNotification(MessageNotification $notification, array $ $notification->withContext(new Support\Context( $message['context']['id'] ?? null, $message['context']['forwarded'] ?? false, + $message['context']['frequently_forwarded'] ?? false, $referred_product ?? null )); } diff --git a/src/WebHook/Notification/Support/Context.php b/src/WebHook/Notification/Support/Context.php index e254d6e..28f4a60 100644 --- a/src/WebHook/Notification/Support/Context.php +++ b/src/WebHook/Notification/Support/Context.php @@ -8,15 +8,19 @@ final class Context private bool $forwarded; + private bool $frequently_forwarded; + private ?ReferredProduct $referred_product; public function __construct( string $replying_to_message_id = null, bool $forwarded = false, + bool $frequently_forwarded = false, ReferredProduct $referred_product = null ) { $this->replying_to_message_id = $replying_to_message_id; $this->forwarded = $forwarded; + $this->frequently_forwarded = $frequently_forwarded; $this->referred_product = $referred_product; } @@ -30,6 +34,11 @@ public function isForwarded(): bool return $this->forwarded; } + public function isFrequentlyForwarded(): bool + { + return $this->frequently_forwarded; + } + public function hasReferredProduct(): bool { return null !== $this->referred_product; From 8d3631c0c65c97c1d7b2a68bbc8baddad1a58e0a Mon Sep 17 00:00:00 2001 From: Alejandro Albarca Date: Fri, 27 Feb 2026 10:05:36 +0100 Subject: [PATCH 07/20] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WebHook/Notification/Support/Context.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WebHook/Notification/Support/Context.php b/src/WebHook/Notification/Support/Context.php index 28f4a60..42ffc86 100644 --- a/src/WebHook/Notification/Support/Context.php +++ b/src/WebHook/Notification/Support/Context.php @@ -13,10 +13,10 @@ final class Context private ?ReferredProduct $referred_product; public function __construct( - string $replying_to_message_id = null, + ?string $replying_to_message_id = null, bool $forwarded = false, bool $frequently_forwarded = false, - ReferredProduct $referred_product = null + ?ReferredProduct $referred_product = null ) { $this->replying_to_message_id = $replying_to_message_id; $this->forwarded = $forwarded; From 35989ba0a3b247ebd14ba8fd8c29c9d9abb790cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 10:07:55 +0100 Subject: [PATCH 08/20] Remove php7.4 support --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a32e80d..bda29f3 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "license": "MIT", "require": { - "php": "^7.4 || ^8.0 || ^8.1", + "php": ">=8.0", "guzzlehttp/guzzle": "^7.0", "vlucas/phpdotenv": "^5.4", "myclabs/php-enum": "^1.8" From 8831aa688614db8079f2085bf19b69663f53d169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 10:10:27 +0100 Subject: [PATCH 09/20] Test "frequently_forwarded" field --- tests/Unit/WebHook/NotificationFactoryTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/WebHook/NotificationFactoryTest.php b/tests/Unit/WebHook/NotificationFactoryTest.php index b21e935..67e929f 100644 --- a/tests/Unit/WebHook/NotificationFactoryTest.php +++ b/tests/Unit/WebHook/NotificationFactoryTest.php @@ -44,6 +44,7 @@ public function test_build_from_payload_can_build_a_notification() "from": "PHONE_NUMBER", "id": "wamid.ID", "forwarded": true, + "frequently_forwarded": true, "referred_product": { "catalog_id": "CATALOG_ID", "product_retailer_id": "PRODUCT_ID" @@ -81,6 +82,7 @@ public function test_build_from_payload_can_build_a_notification() $this->assertEquals('PHONE_NUMBER_ID', $notification->businessPhoneNumberId()); $this->assertEquals('PHONE_NUMBER', $notification->businessPhoneNumber()); $this->assertTrue($notification->isForwarded()); + $this->assertTrue($notification->isFrequentlyForwarded()); $this->assertEquals('WHATSAPP_ID', $notification->customer()->id()); $this->assertEquals('NAME', $notification->customer()->name()); $this->assertEquals('ADID', $notification->referral()->sourceId()); From 36db11d876955e14ea82a5dfdbeb7f421fdbe82d Mon Sep 17 00:00:00 2001 From: so1e <31845646+Yi-pixel@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:40:20 +0800 Subject: [PATCH 10/20] feat: Add type property to Media Class When parsing messages, other types of messages have separate classes, but for media messages, because there is too much overlap, including: sticker, image, document, audio, video, voice, and multiple types, they all use Media Class. However, there is no original type attribute in Media to confirm the type of the message. --- src/Message/Media/MediaType.php | 30 +++++++++++++++++++ src/WebHook/Notification/Media.php | 13 +++++++- .../MessageNotificationFactory.php | 5 +++- .../Unit/WebHook/NotificationFactoryTest.php | 4 +++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/Message/Media/MediaType.php diff --git a/src/Message/Media/MediaType.php b/src/Message/Media/MediaType.php new file mode 100644 index 0000000..2b83cd7 --- /dev/null +++ b/src/Message/Media/MediaType.php @@ -0,0 +1,30 @@ +sha256 = $sha256; $this->filename = $filename; $this->caption = $caption; + $this->type = $type; } public function imageId(): string @@ -57,4 +63,9 @@ public function caption(): string { return $this->caption; } + + public function type(): MediaType + { + return $this->type; + } } diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php index 61fdc68..a009514 100644 --- a/src/WebHook/Notification/MessageNotificationFactory.php +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -2,6 +2,8 @@ namespace Netflie\WhatsAppCloudApi\WebHook\Notification; +use Netflie\WhatsAppCloudApi\Message\Media\MediaType; + class MessageNotificationFactory { public function buildFromPayload(array $metadata, array $message, array $contact): MessageNotification @@ -43,7 +45,8 @@ private function buildMessageNotification(array $metadata, array $message): Mess $message[$message['type']]['sha256'], $message[$message['type']]['filename'] ?? '', $message[$message['type']]['caption'] ?? '', - $message['timestamp'] + $message['timestamp'], + new MediaType($message['type']) ); case 'location': return new Location( diff --git a/tests/Unit/WebHook/NotificationFactoryTest.php b/tests/Unit/WebHook/NotificationFactoryTest.php index 67e929f..56d0b74 100644 --- a/tests/Unit/WebHook/NotificationFactoryTest.php +++ b/tests/Unit/WebHook/NotificationFactoryTest.php @@ -2,6 +2,7 @@ namespace Netflie\WhatsAppCloudApi\Tests\Unit\WebHook; +use Netflie\WhatsAppCloudApi\Message\Media\MediaType; use Netflie\WhatsAppCloudApi\WebHook\Notification; use Netflie\WhatsAppCloudApi\WebHook\NotificationFactory; use PHPUnit\Framework\TestCase; @@ -333,6 +334,7 @@ public function test_build_from_payload_can_build_an_image_notification() $this->assertEquals('IMAGE_HASH', $notification->sha256()); $this->assertEquals('image/jpeg', $notification->mimeType()); $this->assertEquals('CAPTION_TEXT', $notification->caption()); + $this->assertEquals(MediaType::IMAGE(), $notification->type()); } public function test_build_from_payload_can_build_an_document_notification() @@ -381,6 +383,7 @@ public function test_build_from_payload_can_build_an_document_notification() $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $notification->mimeType()); $this->assertEquals('CAPTION_TEXT', $notification->caption()); $this->assertEquals('FILENAME', $notification->filename()); + $this->assertEquals(MediaType::DOCUMENT(), $notification->type()); } public function test_build_from_payload_can_build_a_sticker_notification() @@ -433,6 +436,7 @@ public function test_build_from_payload_can_build_a_sticker_notification() $this->assertEquals('STICKER_ID', $notification->imageId()); $this->assertEquals('STICKER_HASH', $notification->sha256()); $this->assertEquals('image/webp', $notification->mimeType()); + $this->assertEquals(MediaType::STICKER(), $notification->type()); } public function test_build_from_payload_can_build_an_unknown_notification() From 63562845fb50911d994cb90aa9d7abafe545596e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 10:25:03 +0100 Subject: [PATCH 11/20] MessageNotificationFactory now is a final class --- src/WebHook/Notification/MessageNotificationFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php index a009514..ed6c3bf 100644 --- a/src/WebHook/Notification/MessageNotificationFactory.php +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Media\MediaType; -class MessageNotificationFactory +final class MessageNotificationFactory { public function buildFromPayload(array $metadata, array $message, array $contact): MessageNotification { From a3ebd5943b1f941dca7b9c97a77e3fd50d140956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 10:29:11 +0100 Subject: [PATCH 12/20] Changed order for MediaType constructor parameter --- src/WebHook/Notification/Media.php | 4 ++-- src/WebHook/Notification/MessageNotificationFactory.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WebHook/Notification/Media.php b/src/WebHook/Notification/Media.php index acc2771..9a57d26 100644 --- a/src/WebHook/Notification/Media.php +++ b/src/WebHook/Notification/Media.php @@ -26,8 +26,8 @@ public function __construct( string $sha256, string $filename, string $caption, - string $received_at_timestamp, - MediaType $type + MediaType $type, + string $received_at_timestamp ) { parent::__construct($id, $business, $received_at_timestamp); diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php index ed6c3bf..2b19f0a 100644 --- a/src/WebHook/Notification/MessageNotificationFactory.php +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -45,8 +45,8 @@ private function buildMessageNotification(array $metadata, array $message): Mess $message[$message['type']]['sha256'], $message[$message['type']]['filename'] ?? '', $message[$message['type']]['caption'] ?? '', - $message['timestamp'], - new MediaType($message['type']) + new MediaType($message['type']), + $message['timestamp'] ); case 'location': return new Location( From 0b1d5b0c4f4a3d4eae3b2d79f118df095fab8399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 10:46:05 +0100 Subject: [PATCH 13/20] Test webhook notification without user profile --- .../Unit/WebHook/NotificationFactoryTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Unit/WebHook/NotificationFactoryTest.php b/tests/Unit/WebHook/NotificationFactoryTest.php index 56d0b74..32701c4 100644 --- a/tests/Unit/WebHook/NotificationFactoryTest.php +++ b/tests/Unit/WebHook/NotificationFactoryTest.php @@ -1452,4 +1452,41 @@ public function test_build_from_payload_can_build_a_service_status_notification( $this->assertTrue($notification->isMessageDelivered()); $this->assertTrue($notification->isMessageSent()); } + + public function test_build_from_payload_without_contact_profile_can_build_a_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "wa_id": "PHONE_NUMBER" + }], + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "text": { + "body": "MESSAGE_BODY" + }, + "type": "text" + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Text::class, $notification); + $this->assertEquals('MESSAGE_BODY', $notification->message()); + } } From 90e1bfd0e53f97a7f650ea9538da5774a5ef6290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 10:48:01 +0100 Subject: [PATCH 14/20] Update Github workflow to remove php7.4 support --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index f34af0d..32526b4 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 8.0, 8.1] + php: [8.0, 8.1, 8.2, 8.3, 8.4] name: Tests on PHP ${{ matrix.php }} - ${{ matrix.stability }} From ebf5cd6ac1ae53fed611d9296d6979c78d859f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 11:03:29 +0100 Subject: [PATCH 15/20] Fix webhook tests to php8.3 compatibility --- src/WebHook/VerificationRequest.php | 19 +++++++++++++++++-- .../Unit/WebHook/VerificationRequestTest.php | 4 ++-- tests/Unit/WebHookTest.php | 1 - 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/WebHook/VerificationRequest.php b/src/WebHook/VerificationRequest.php index 1d8b976..56be232 100644 --- a/src/WebHook/VerificationRequest.php +++ b/src/WebHook/VerificationRequest.php @@ -9,6 +9,7 @@ final class VerificationRequest * @link https://developers.facebook.com/docs/graph-api/webhooks/getting-started?locale=en_US#configure-webhooks-product */ protected string $verify_token; + private int $response_code = 200; public function __construct(string $verify_token) { @@ -22,13 +23,27 @@ public function validate(array $payload): string $challenge = $payload['hub_challenge'] ?? ''; if ('subscribe' !== $mode || $token !== $this->verify_token) { - http_response_code(403); + $this->setResponseCode(403); return $challenge; } - http_response_code(200); + $this->setResponseCode(200); return $challenge; } + + public function responseCode(): int + { + return $this->response_code; + } + + private function setResponseCode(int $response_code): void + { + $this->response_code = $response_code; + + if (!headers_sent()) { + http_response_code($response_code); + } + } } diff --git a/tests/Unit/WebHook/VerificationRequestTest.php b/tests/Unit/WebHook/VerificationRequestTest.php index 8f0a26f..9adb16e 100644 --- a/tests/Unit/WebHook/VerificationRequestTest.php +++ b/tests/Unit/WebHook/VerificationRequestTest.php @@ -21,7 +21,7 @@ public function test_verification_request_succeeded() ]); $this->assertEquals('challenge_code', $response); - $this->assertEquals(200, http_response_code()); + $this->assertEquals(200, $verification_request->responseCode()); } public function test_verification_request_fails() @@ -35,6 +35,6 @@ public function test_verification_request_fails() ]); $this->assertEquals('challenge_code', $response); - $this->assertEquals(403, http_response_code()); + $this->assertEquals(403, $verification_request->responseCode()); } } diff --git a/tests/Unit/WebHookTest.php b/tests/Unit/WebHookTest.php index a6e2bb2..7d79251 100644 --- a/tests/Unit/WebHookTest.php +++ b/tests/Unit/WebHookTest.php @@ -28,6 +28,5 @@ public function test_verify_a_webhook() ], 'verify-token'); $this->assertEquals('challenge_code', $response); - $this->assertEquals(200, http_response_code()); } } From 128c1239ea69c3c215c22658ad4e1fae5a05b81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 11:11:02 +0100 Subject: [PATCH 16/20] Support new phpunit versions --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bda29f3..f213ec8 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "myclabs/php-enum": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": ">=10.0", "symfony/var-dumper": "^5.0", "phpspec/prophecy-phpunit": "^2.0", "fakerphp/faker": "^1.19", From 167864fde046ec3cce8601f022a21afe038cc690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 11:30:33 +0100 Subject: [PATCH 17/20] Update phpunit.xml to new v12 schema --- .gitignore | 3 ++- composer.json | 4 ++-- phpunit.xml.dist | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index b1f7502..a201441 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ composer.phar .phpunit.result.cache composer.lock .php-cs-fixer.cache -.idea/ \ No newline at end of file +.idea/ +.phpunit.cache \ No newline at end of file diff --git a/composer.json b/composer.json index f213ec8..90964b1 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ } }, "scripts": { - "unit-test": "vendor/bin/phpunit --group unit", - "integration-test": "vendor/bin/phpunit --group integration" + "unit-test": "vendor/bin/phpunit --testsuite unit", + "integration-test": "vendor/bin/phpunit --testsuite integration" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 79355f1..172707a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,5 @@ - - - - src/ - - + tests/Unit @@ -16,4 +11,9 @@ - \ No newline at end of file + + + src/ + + + From 8f8936f0e246bb96d0404b719b2d8498b47ecdb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 27 Feb 2026 11:37:04 +0100 Subject: [PATCH 18/20] Remove support for php8.0v --- .github/workflows/php.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 32526b4..4caf803 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.0, 8.1, 8.2, 8.3, 8.4] + php: [8.1, 8.2, 8.3, 8.4] name: Tests on PHP ${{ matrix.php }} - ${{ matrix.stability }} diff --git a/composer.json b/composer.json index 90964b1..7b61dd6 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ ], "license": "MIT", "require": { - "php": ">=8.0", + "php": ">=8.1", "guzzlehttp/guzzle": "^7.0", "vlucas/phpdotenv": "^5.4", "myclabs/php-enum": "^1.8" From 22b53644184fdaf925747fcbd675302fe4b5dd78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:25:23 +0000 Subject: [PATCH 19/20] Initial plan From 3e6f9d5d36c15e4acf16c1ac3097614acb45bde0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:29:08 +0000 Subject: [PATCH 20/20] Update UPGRADE.md with 2.x to 3.x migration instructions Co-authored-by: aalbarca <1410345+aalbarca@users.noreply.github.com> --- UPGRADE.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/UPGRADE.md b/UPGRADE.md index 6200c99..7993d28 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,7 +1,132 @@ -# Upgrade to v2 +# Upgrade Guide All instructions to upgrade this project from one major version to the next will be documented in this file. Upgrades must be run sequentially, meaning you should not skip major releases while upgrading (fix releases can be skipped). +## 2.x to 3.x + +### PHP version requirement + +PHP 7.4 and PHP 8.0 are no longer supported. The minimum required PHP version is now **PHP 8.1**. Update your environment accordingly before upgrading. + +### `MessageNotificationFactory` is now `final` + +`Netflie\WhatsAppCloudApi\WebHook\Notification\MessageNotificationFactory` has been marked as `final`. If you have extended this class, you must refactor your code to use composition instead of inheritance. + +### `Media` constructor signature changed + +A new required `MediaType $type` parameter has been added to the `Media` constructor. If you are instantiating `Netflie\WhatsAppCloudApi\WebHook\Notification\Media` directly, you must pass a `Netflie\WhatsAppCloudApi\Message\Media\MediaType` instance as the eighth argument (before `$received_at_timestamp`). + +Before: +```php +new Media($id, $business, $image_id, $mime_type, $sha256, $filename, $caption, $received_at_timestamp); +``` + +After: +```php +use Netflie\WhatsAppCloudApi\Message\Media\MediaType; + +new Media($id, $business, $image_id, $mime_type, $sha256, $filename, $caption, new MediaType('image'), $received_at_timestamp); +``` + +### `Context` constructor signature changed + +A new `bool $frequently_forwarded` parameter has been added to the `Context` constructor between `$forwarded` and `$referred_product`. If you are instantiating `Netflie\WhatsAppCloudApi\WebHook\Notification\Support\Context` directly, update your call accordingly. + +Before: +```php +new Context($replying_to_message_id, $forwarded, $referred_product); +``` + +After: +```php +new Context($replying_to_message_id, $forwarded, $frequently_forwarded, $referred_product); +``` + +### `Referral` constructor signature changed + +A new required `string $ctwa_clid` parameter has been appended to the `Referral` constructor. If you are instantiating `Netflie\WhatsAppCloudApi\WebHook\Notification\Support\Referral` directly, pass the Click to WhatsApp click ID as the last argument. + +Before: +```php +new Referral($source_id, $source_url, $source_type, $headline, $body, $media_type, $media_url, $thumbnail_url); +``` + +After: +```php +new Referral($source_id, $source_url, $source_type, $headline, $body, $media_type, $media_url, $thumbnail_url, $ctwa_clid); +``` + +### New features + +The following new features are available in 3.x: + +#### Send Single Product Message + +```php +$whatsapp_cloud_api->sendSingleProduct( + '', + '', + '', + 'Optional body text', + 'Optional footer text' +); +``` + +#### Create and Update Templates + +Template management now requires a `business_id` in the `WhatsAppCloudApi` constructor: + +```php +$whatsapp = new WhatsAppCloudApi([ + 'from_phone_number_id' => 'your-phone-number-id', + 'access_token' => 'your-access-token', + 'business_id' => 'your-business-id', +]); + +// Create a template +$whatsapp->createTemplate('template_name', 'MARKETING', 'en_US', $components); + +// Update a template by ID +$whatsapp->updateTemplateById('', $payload); +``` + +#### Frequently forwarded messages + +`MessageNotification` and `Context` now expose an `isFrequentlyForwarded()` method: + +```php +$notification->isFrequentlyForwarded(); +$notification->context()->isFrequentlyForwarded(); +``` + +#### Media type in Webhook notifications + +`Media` notifications now expose the media type via `type()`, which returns a `MediaType` enum value: + +```php +$notification->type(); // returns a MediaType enum instance +``` + +#### Click to WhatsApp click ID in Referral + +`Referral` now exposes the `ctwaClid()` method: + +```php +$notification->referral()->ctwaClid(); +``` + +#### `VerificationRequest` response code + +`VerificationRequest::verify()` no longer calls `http_response_code()` when headers have already been sent. You can now read the resulting HTTP status code via the new `responseCode()` method: + +```php +$verificationRequest = new VerificationRequest($verify_token); +$challenge = $verificationRequest->verify($payload); +$code = $verificationRequest->responseCode(); // 200 or 403 +``` + +--- + ## 1.x to 2.x # Final classes