Author SHA1 Message Date
dependabot[bot] f6c763d1bf chore(deps): bump the php-security group across 1 directory with 10 updates
Bumps the php-security group with 3 updates in the / directory: [guzzlehttp/guzzle](https://github.com/guzzle/guzzle), [laravel/framework](https://github.com/laravel/framework) and [symfony/yaml](https://github.com/symfony/yaml).


Updates `guzzlehttp/guzzle` from 7.10.0 to 7.15.2
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/7.15.2/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.10.0...7.15.2)

Updates `guzzlehttp/psr7` from 2.9.0 to 2.13.0
- [Release notes](https://github.com/guzzle/psr7/releases)
- [Changelog](https://github.com/guzzle/psr7/blob/3.0/CHANGELOG.md)
- [Commits](https://github.com/guzzle/psr7/compare/2.9.0...2.13.0)

Updates `laravel/framework` from 12.56.0 to 12.64.0
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/13.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v12.56.0...v12.64.0)

Updates `symfony/http-foundation` from 7.4.8 to 7.4.14
- [Release notes](https://github.com/symfony/http-foundation/releases)
- [Changelog](https://github.com/symfony/http-foundation/blob/8.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/http-foundation/compare/v7.4.8...v7.4.14)

Updates `symfony/http-kernel` from 7.4.8 to 7.4.14
- [Release notes](https://github.com/symfony/http-kernel/releases)
- [Changelog](https://github.com/symfony/http-kernel/blob/8.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/http-kernel/compare/v7.4.8...v7.4.14)

Updates `symfony/mailer` from 7.4.8 to 7.4.14
- [Release notes](https://github.com/symfony/mailer/releases)
- [Changelog](https://github.com/symfony/mailer/blob/8.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/mailer/compare/v7.4.8...v7.4.14)

Updates `symfony/mime` from 7.4.8 to 7.4.13
- [Release notes](https://github.com/symfony/mime/releases)
- [Changelog](https://github.com/symfony/mime/blob/8.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/mime/compare/v7.4.8...v7.4.13)

Updates `symfony/polyfill-intl-idn` from 1.33.0 to 1.38.1
- [Release notes](https://github.com/symfony/polyfill-intl-idn/releases)
- [Commits](https://github.com/symfony/polyfill-intl-idn/compare/v1.33.0...v1.38.1)

Updates `symfony/routing` from 7.4.8 to 7.4.13
- [Release notes](https://github.com/symfony/routing/releases)
- [Changelog](https://github.com/symfony/routing/blob/8.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/routing/compare/v7.4.8...v7.4.13)

Updates `symfony/yaml` from 7.4.8 to 7.4.14
- [Release notes](https://github.com/symfony/yaml/releases)
- [Changelog](https://github.com/symfony/yaml/blob/8.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/yaml/compare/v7.4.8...v7.4.14)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-version: 7.15.2
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: guzzlehttp/psr7
  dependency-version: 2.13.0
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: laravel/framework
  dependency-version: 12.64.0
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/http-foundation
  dependency-version: 7.4.14
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/http-kernel
  dependency-version: 7.4.14
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/mailer
  dependency-version: 7.4.14
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/mime
  dependency-version: 7.4.13
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/polyfill-intl-idn
  dependency-version: 1.38.1
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/routing
  dependency-version: 7.4.13
  dependency-type: indirect
  dependency-group: php-security
- dependency-name: symfony/yaml
  dependency-version: 7.4.14
  dependency-type: indirect
  dependency-group: php-security
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:22:41 +00:00
pixel-workflows[bot] 885385a878 chore: sync files from wearepixel/pixel-workflows - created local 'pint.json' from remote 'files/pint.json' 2026-07-27 08:21:18 +00:00
pixel-workflows[bot] dc7d1bd7b5 chore: sync files from wearepixel/pixel-workflows - created local '.gitattributes' from remote 'files/.gitattributes' 2026-07-27 08:21:15 +00:00
pixel-workflows[bot] 719edb76f2 chore: sync files from wearepixel/pixel-workflows - created local '.editorconfig' from remote 'files/.editorconfig' 2026-07-27 08:21:12 +00:00
pixel-workflows[bot] cc874d330e chore: sync files from wearepixel/pixel-workflows - created local '.github/workflows/dependabot-slack-alert.yml' from remote 'files/.github/workflows/dependabot-slack-alert.yml' 2026-07-27 08:21:09 +00:00
pixel-workflows[bot] 44eb95a49b chore: sync files from wearepixel/pixel-workflows - created local '.github/workflows/dependabot-auto-merge.yml' from remote 'files/.github/workflows/dependabot-auto-merge.yml' 2026-07-27 08:21:06 +00:00
pixel-workflows[bot] 8a8628e73d chore: update dependabot configuration 2026-07-27 08:20:51 +00:00
Joel Male e5151ee858 Merge pull request #1 from wearepixel/security/pin-actions-shas
chore: pin GitHub Actions to full commit SHAs
2026-05-27 15:00:46 +10:00
Joel Male 543b124ff7 Remove npm configuration from dependabot.yml
Removed npm dependency configuration from Dependabot.
2026-05-27 14:54:15 +10:00
Joel Male 9148b2244d Configure Dependabot for multiple package ecosystems
Added configurations for GitHub Actions, Composer, and npm package ecosystems in Dependabot.
2026-05-27 14:53:54 +10:00
Joel Male 9ed9a0a2da chore: pin GitHub Actions to full commit SHAs
Pins all GitHub Actions uses: references to full commit SHAs
to prevent supply chain attacks via mutable action tags.
Original tag references are kept as inline comments.
2026-05-27 14:46:59 +10:00
Joel Male 32ea3b5a81 Fix formatting in SECURITY.md for vulnerability reporting 2026-05-27 14:46:36 +10:00
Joel Male 37a5d81a9c Revise security policy and reporting guidelines
Removed supported versions table and updated reporting instructions.
2026-05-27 14:45:27 +10:00
Joel Male 7528539779 ci: add GitHub Actions workflow and update README 2026-04-11 08:42:22 +10:00
Joel Male 33b0903b04 test: add Pest test suite for LaravelFacebookFeed 2026-04-11 08:41:06 +10:00
Joel Male 11daeba6fb chore: add Pest v4 testing infrastructure with Orchestra Testbench 2026-04-11 08:39:39 +10:00
Joel Male d43769dd98 refactor: strict types, typed props, split toXml/generate, custom exception 2026-04-11 08:37:43 +10:00
Joel Male d640477b17 chore: support Laravel 10 & 11 2024-01-08 09:27:18 +10:00
Joel Male 7597444f21 version bump 2023-01-17 13:26:20 +10:00
Joel Male 6c70016a90 update v1.0.0 2023-01-17 13:23:45 +10:00
17 changed files with 9532 additions and 319 deletions
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4
+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+38
View File
@@ -0,0 +1,38 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 10
groups:
actions:
applies-to: version-updates
patterns: ["*"]
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
day: "tuesday"
open-pull-requests-limit: 10
groups:
php-minor-patch:
applies-to: version-updates
update-types:
- "minor"
- "patch"
php-security:
applies-to: security-updates
patterns: ["*"]
commit-message:
prefix: "chore"
include: "scope"
labels:
- "dependencies"
@@ -0,0 +1,59 @@
name: Dependabot Auto-merge
on:
workflow_run:
workflows: ["Tests"]
types: [completed]
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.actor.login == 'dependabot[bot]'
steps:
- name: Find PR and check update type
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
PR_DATA=$(gh pr list --repo "${{ github.repository }}" --head "$HEAD_BRANCH" --json number,title,url,author --jq '.[0]')
PR_URL=$(echo "$PR_DATA" | jq -r '.url')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
if [ "$PR_AUTHOR" != "app/dependabot" ]; then
echo "is_safe=false" >> $GITHUB_OUTPUT
echo "Not a Dependabot PR, skipping."
exit 0
fi
# Parse major version numbers from title: "bump X from A[.B.C] to D[.E.F]"
# Works for both "from 4.0.0 to 4.1.0" and "from 4 to 6" (GitHub Actions)
FROM_MAJOR=$(echo "$PR_TITLE" | grep -oP 'from \K[0-9]+')
TO_MAJOR=$(echo "$PR_TITLE" | grep -oP 'to \K[0-9]+')
echo "PR: $PR_TITLE (major: $FROM_MAJOR -> $TO_MAJOR)"
if [ "$FROM_MAJOR" = "$TO_MAJOR" ]; then
echo "is_safe=true" >> $GITHUB_OUTPUT
else
echo "is_safe=false" >> $GITHUB_OUTPUT
echo "Major version bump, skipping auto-merge."
fi
- name: Merge minor and patch updates
if: steps.check.outputs.is_safe == 'true'
run: gh pr merge --merge "${{ steps.check.outputs.pr_url }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,98 @@
name: Dependabot Slack Alert
on:
pull_request_target:
types: [opened]
jobs:
notify:
name: Notify Slack of major update
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Check if major version bump
id: check
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
from_major=$(echo "$PR_TITLE" | grep -oP 'from \K[0-9]+' | head -1)
to_major=$(echo "$PR_TITLE" | grep -oP 'to \K[0-9]+' | head -1)
if [ -n "$from_major" ] && [ -n "$to_major" ] && [ "$from_major" != "$to_major" ]; then
echo "is_major=true" >> $GITHUB_OUTPUT
echo "Major bump detected: $from_major -> $to_major"
else
echo "is_major=false" >> $GITHUB_OUTPUT
echo "Minor/patch bump, skipping"
fi
- name: Send Slack alert
if: steps.check.outputs.is_major == 'true'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "SLACK_WEBHOOK_URL not configured for this repo, skipping"
exit 0
fi
payload=$(jq -n \
--arg repo "$REPO" \
--arg title "$PR_TITLE" \
--arg url "$PR_URL" \
--arg pr "#$PR_NUMBER" \
'{
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: ":dependabot: *Major dependency update requires review*"
}
},
{
type: "section",
fields: [
{type: "mrkdwn", text: ("*Repository*\n`" + $repo + "`")},
{type: "mrkdwn", text: ("*Pull Request*\n<" + $url + "|" + $pr + ">") }
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: $title
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: "Major version bumps are not auto-merged as they may contain breaking changes."
}
]
},
{
type: "actions",
elements: [
{
type: "button",
text: {type: "plain_text", text: "Review PR"},
url: $url,
style: "primary"
}
]
}
]
}')
curl -s -o /dev/null -w "%{http_code}" -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-type: application/json" \
--data "$payload" | grep -q "^200$" \
&& echo "Slack notification sent" \
|| echo "Slack notification failed"
+34
View File
@@ -0,0 +1,34 @@
name: Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: ["8.4"]
name: PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup PHP
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run tests
run: vendor/bin/pest
+65 -14
View File
@@ -1,33 +1,84 @@
Laravel Facebook Feed # Laravel Facebook Feed
==============
A simple package to easily create an XML feed for Facebooks Shipping API to parse and retrieve your products on a frequent basis. [![Latest Version on Packagist](https://img.shields.io/packagist/v/wearepixel/laravel-facebook-feed.svg)](https://packagist.org/packages/wearepixel/laravel-facebook-feed)
[![Tests](https://img.shields.io/github/actions/workflow/status/wearepixel/laravel-facebook-feed/tests.yml?label=tests)](https://github.com/wearepixel/laravel-facebook-feed/actions/workflows/tests.yml)
[![PHP Version](https://img.shields.io/packagist/php-v/wearepixel/laravel-facebook-feed.svg)](https://packagist.org/packages/wearepixel/laravel-facebook-feed)
[![Total Downloads](https://img.shields.io/packagist/dt/wearepixel/laravel-facebook-feed.svg)](https://packagist.org/packages/wearepixel/laravel-facebook-feed)
[![License](https://img.shields.io/packagist/l/wearepixel/laravel-facebook-feed.svg)](https://packagist.org/packages/wearepixel/laravel-facebook-feed)
We recommend adding this to an API controller and generating it on the fly each time so you can be sure your products are always up to date within your merchant center. A simple package to easily create an XML feed for the Facebook Catalog API to parse and retrieve your products.
We recommend adding this to an API controller and generating it on the fly so your feed is always up to date.
## Requirements
- PHP 8.4+
- Laravel 10, 11, or 12
## Installation ## Installation
``` ```bash
composer require wearepixel/laravel-facebook-feed composer require wearepixel/laravel-facebook-feed
``` ```
## Example ## Usage
```php ```php
use Wearepixel\LaravelFacebookFeed\LaravelFacebookFeed; use Wearepixel\LaravelFacebookFeed\LaravelFacebookFeed;
$feed = LaravelFacebookFeed::init( $feed = LaravelFacebookFeed::init(
'Product Feed', title: 'My Store',
'App product Feed', description: 'My store product feed',
'https://mystore.com' link: 'https://mystore.com',
); );
$feed->addItem([ $feed->addItem([
'id' => 'item_001', 'id' => 'item_001',
'title' => 'Blue Nikes', 'title' => 'Blue Nikes',
'link' => 'https://mystore.com/products/blue-nikes', 'link' => 'https://mystore.com/products/blue-nikes',
'price' => 29.99, 'price' => '29.99 AUD',
'image_link' => 'https://mystore.com/images/blue-nikes-001.jpg', 'image_link' => 'https://mystore.com/images/blue-nikes-001.jpg',
]); ]);
return $feed->generate(); return $feed->generate();
``` ```
### Required item fields
| Field | Description |
| ------------ | ------------------------------------------------ |
| `id` | Unique product identifier |
| `title` | Product name |
| `link` | URL to the product page |
| `price` | Price including currency code (e.g. `29.99 AUD`) |
| `image_link` | URL to the main product image |
Any additional fields supported by the [Facebook Product Feed Specification](https://www.facebook.com/business/help/120325381656392) can be passed alongside the required fields.
### Methods
**`LaravelFacebookFeed::init(string $title, string $description, string $link): static`**
Creates a new feed instance with channel metadata.
**`$feed->addItem(array $item): void`**
Adds a product to the feed. Throws `MissingRequiredFieldException` if any required field is absent.
**`$feed->toXml(): string`**
Returns the feed as an XML string.
**`$feed->generate(): Response`**
Returns an HTTP response with the XML content and `application/rss+xml` content type. Suitable for returning directly from a controller.
## Testing
```bash
composer test
```
## License
MIT
+11
View File
@@ -0,0 +1,11 @@
# Security Policy
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
## Reporting a Vulnerability
If you discover a security vulnerability, please report it privately using the below channel:
**GitHub Private Vulnerability Reporting** — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
All security vulnerabilities will be promptly addressed.
+44 -24
View File
@@ -1,26 +1,46 @@
{ {
"name": "wearepixel/laravel-facebook-feed", "name": "wearepixel/laravel-facebook-feed",
"type": "library", "type": "library",
"description": "Laravel Facebook Feed", "description": "Laravel Facebook Feed",
"keywords": [ "keywords": [
"laravel", "laravel",
"laravel-facebook-feed" "laravel-facebook-feed"
], ],
"version": "3.0.0", "version": "2.0.0",
"homepage": "https://github.com/wearepixel/laravel-facebook-feed", "homepage": "https://github.com/wearepixel/laravel-facebook-feed",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "Pixel", "name": "Pixel",
"email": "hello@wearepixel.com.au" "email": "hello@wearepixel.com.au"
} }
], ],
"autoload": { "autoload": {
"psr-4": { "Wearepixel\\LaravelFacebookFeed\\" : "src/" } "psr-4": {
}, "Wearepixel\\LaravelFacebookFeed\\": "src/"
"require": { }
"spatie/array-to-xml": "2.8.*|3.*.*", },
"illuminate/support": "8.*|9.*" "autoload-dev": {
}, "psr-4": {
"prefer-stable": true "Tests\\": "tests/"
}
},
"require": {
"php": ">=8.4",
"spatie/array-to-xml": "2.8.*|3.*",
"illuminate/support": "10.*|11.*|12.*|13.*"
},
"require-dev": {
"pestphp/pest": "^4.0",
"orchestra/testbench": "^10.0"
},
"scripts": {
"test": "vendor/bin/pest"
},
"prefer-stable": true,
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
}
} }
Generated
+8915 -245
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
+13
View File
@@ -0,0 +1,13 @@
{
"preset": "laravel",
"rules": {
"concat_space": {
"spacing": "one"
},
"class_attributes_separation": false,
"ordered_imports": {
"sort_algorithm": "length"
},
"not_operator_with_space": false
}
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Wearepixel\LaravelFacebookFeed\Exceptions;
use RuntimeException;
class MissingRequiredFieldException extends RuntimeException {}
+33 -35
View File
@@ -1,31 +1,33 @@
<?php <?php
declare(strict_types=1);
namespace Wearepixel\LaravelFacebookFeed; namespace Wearepixel\LaravelFacebookFeed;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Response as ResponseFacade;
use Spatie\ArrayToXml\ArrayToXml; use Spatie\ArrayToXml\ArrayToXml;
use Wearepixel\LaravelFacebookFeed\Exceptions\MissingRequiredFieldException;
class LaravelFacebookFeed class LaravelFacebookFeed
{ {
public $title; public readonly string $title;
public $description; public readonly string $description;
public $link; public readonly string $link;
protected $xml = []; protected array $products = [];
protected $products = [];
protected $currency = "AUD";
protected $requiredProductFields = [ protected const array REQUIRED_PRODUCT_FIELDS = [
'id', 'id',
'link', 'link',
'title', 'title',
'price', 'price',
'image_link' 'image_link',
]; ];
public static function init($title = null, $description = null, $link = null) public static function init(string $title = '', string $description = '', string $link = ''): static
{ {
$feed = new self(); $feed = new static;
$feed->title = $title; $feed->title = $title;
$feed->description = $description; $feed->description = $description;
$feed->link = $link; $feed->link = $link;
@@ -33,26 +35,20 @@ public static function init($title = null, $description = null, $link = null)
return $feed; return $feed;
} }
public function addItem(array $item): bool public function addItem(array $item): void
{ {
foreach ($this->requiredProductFields as $field) { foreach (self::REQUIRED_PRODUCT_FIELDS as $field) {
if (!isset($item[$field])) { if (! isset($item[$field])) {
throw new \Exception("Required field '{$field}' is missing"); throw new MissingRequiredFieldException("Required field '{$field}' is missing");
} }
} }
foreach ($item as $key => $value) { $this->products[] = $item;
$product[$key] = $value;
}
$this->products[] = $product;
return true;
} }
public function generate() public function toXml(): string
{ {
$this->xml = [ $data = [
'rss' => [ 'rss' => [
'_attributes' => [ '_attributes' => [
'xmlns:g' => 'http://base.google.com/ns/1.0', 'xmlns:g' => 'http://base.google.com/ns/1.0',
@@ -62,23 +58,25 @@ public function generate()
'title' => $this->title, 'title' => $this->title,
'description' => $this->description, 'description' => $this->description,
'link' => $this->link, 'link' => $this->link,
] ],
] ],
]; ];
foreach ($this->products as $key => $product) { foreach ($this->products as $key => $product) {
$this->xml['rss']['channel']['item_'.$key] = $product; $data['rss']['channel']['item_' . $key] = $product;
} }
$xml = ArrayToXml::convert($this->xml, ''); $xml = ArrayToXml::convert($data, '');
$xml = str_replace([' ', '<root>', '</root>', "\n", "\r", '<remove>remove</remove>'], '', $xml); $xml = str_replace([' ', '<root>', '</root>', "\n", "\r", '<remove>remove</remove>'], '', $xml);
$xml = preg_replace([ // spatie/array-to-xml does not support repeated keys, so products are keyed as
"/item_[0-9][0-9][0-9][0-9]/", // item_0, item_1, etc. and renamed back to <item> tags here.
"/item_[0-9][0-9][0-9]/", $xml = (string) preg_replace('/item_\d+/', 'item', $xml);
"/item_[0-9][0-9]/",
"/item_[0-9]/",
], "item", $xml);
return response($xml)->header('Content-Type', 'text/xml'); return $xml;
}
public function generate(): Response
{
return ResponseFacade::make($this->toXml(), headers: ['Content-Type' => 'application/rss+xml']);
} }
} }
+157
View File
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
use Wearepixel\LaravelFacebookFeed\Exceptions\MissingRequiredFieldException;
use Wearepixel\LaravelFacebookFeed\LaravelFacebookFeed;
function validProduct(array $overrides = []): array
{
return array_merge([
'id' => 'prod-1',
'link' => 'https://example.com/product',
'title' => 'Test Product',
'price' => '29.99 AUD',
'image_link' => 'https://example.com/image.jpg',
], $overrides);
}
describe('init', function () {
it('stores title, description, and link', function () {
$feed = LaravelFacebookFeed::init('My Store', 'My Desc', 'https://example.com');
expect($feed->title)->toBe('My Store')
->and($feed->description)->toBe('My Desc')
->and($feed->link)->toBe('https://example.com');
});
it('defaults to empty strings', function () {
$feed = LaravelFacebookFeed::init();
expect($feed->title)->toBe('')
->and($feed->description)->toBe('')
->and($feed->link)->toBe('');
});
});
describe('addItem', function () {
it('accepts a valid item without throwing', function () {
$feed = LaravelFacebookFeed::init('', '', '');
$feed->addItem(validProduct());
expect($feed->toXml())->toContain('prod-1');
});
it('throws for missing required field', function (string $field) {
$product = validProduct();
unset($product[$field]);
expect(fn () => LaravelFacebookFeed::init()->addItem($product))
->toThrow(MissingRequiredFieldException::class);
})->with(['id', 'link', 'title', 'price', 'image_link']);
it('throws with a message containing the field name', function () {
$product = validProduct();
unset($product['price']);
expect(fn () => LaravelFacebookFeed::init()->addItem($product))
->toThrow(MissingRequiredFieldException::class, 'price');
});
});
describe('toXml', function () {
it('returns a string', function () {
$feed = LaravelFacebookFeed::init('', '', '');
expect($feed->toXml())->toBeString();
});
it('contains the RSS xmlns:g attribute', function () {
$feed = LaravelFacebookFeed::init('', '', '');
expect($feed->toXml())->toContain('xmlns:g="http://base.google.com/ns/1.0"');
});
it('contains the RSS version attribute', function () {
$feed = LaravelFacebookFeed::init('', '', '');
expect($feed->toXml())->toContain('version="2.0"');
});
it('includes the channel title, description and link', function () {
$feed = LaravelFacebookFeed::init('My Store', 'My Desc', 'https://example.com');
$xml = $feed->toXml();
expect($xml)
->toContain('<title>My Store</title>')
->toContain('<description>My Desc</description>')
->toContain('<link>https://example.com</link>');
});
it('includes product data', function () {
$feed = LaravelFacebookFeed::init('', '', '');
$feed->addItem(validProduct());
$xml = $feed->toXml();
expect($xml)
->toContain('<id>prod-1</id>')
->toContain('<title>Test Product</title>');
});
it('wraps products in item tags', function () {
$feed = LaravelFacebookFeed::init('', '', '');
$feed->addItem(validProduct());
expect($feed->toXml())
->toContain('<item>')
->toContain('</item>');
});
it('handles multiple products', function () {
$feed = LaravelFacebookFeed::init('', '', '');
$feed->addItem(validProduct(['id' => 'prod-1']));
$feed->addItem(validProduct(['id' => 'prod-2']));
$xml = $feed->toXml();
expect($xml)
->toContain('prod-1')
->toContain('prod-2');
expect(substr_count($xml, '<item>'))->toBe(2);
});
it('includes product price and image_link fields', function () {
$feed = LaravelFacebookFeed::init('', '', '');
$feed->addItem(validProduct());
$xml = $feed->toXml();
expect($xml)
->toContain('<price>29.99 AUD</price>')
->toContain('<image_link>https://example.com/image.jpg</image_link>');
});
it('generates valid xml with no products', function () {
$feed = LaravelFacebookFeed::init('My Store', 'My Desc', 'https://example.com');
$xml = $feed->toXml();
expect($xml)
->toBeString()
->toContain('<title>My Store</title>')
->not->toContain('<item>');
});
it('includes the xml declaration', function () {
$feed = LaravelFacebookFeed::init('', '', '');
expect($feed->toXml())->toStartWith('<?xml');
});
it('generates parseable xml', function () {
$feed = LaravelFacebookFeed::init('Store', 'Desc', 'https://example.com');
$feed->addItem(validProduct());
$result = simplexml_load_string($feed->toXml());
expect($result)->not->toBeFalse();
});
});
describe('generate', function () {
it('returns an HTTP 200 response', function () {
$feed = LaravelFacebookFeed::init('', '', '');
expect($feed->generate()->getStatusCode())->toBe(200);
});
it('sets the content type to application/rss+xml', function () {
$feed = LaravelFacebookFeed::init('', '', '');
expect($feed->generate()->headers->get('Content-Type'))->toContain('application/rss+xml');
});
it('returns the xml in the response body', function () {
$feed = LaravelFacebookFeed::init('Store', 'Desc', 'https://example.com');
$feed->addItem(validProduct());
expect($feed->generate()->getContent())->toBe($feed->toXml());
});
});
+5
View File
@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
uses(Tests\TestCase::class)->in('.');
+9
View File
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Tests;
use Orchestra\Testbench\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase {}