Skip to content

Commit f8d29f0

Browse files
authored
Merge pull request #11 from phpform-dev/webhooks
Webhooks
2 parents 3e827f5 + 0f7794a commit f8d29f0

17 files changed

+597
-54
lines changed

Diff for: CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
0.3.0
2+
==
3+
17 March 2024
4+
5+
**Added:**
6+
* Webhooks support
7+
8+
**Fixed:**
9+
* Export to Excel/CSV
10+
111
0.2.3
212
==
313
27 Frb 2024

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# PHPForm - Lightweight Headless Form Builder and API
1+
# PHPForm - Open Source Headless Form Management Server
22

33
Welcome to PHPForm, a fully open-source, headless form builder designed with simplicity, efficiency, and privacy in mind.
44
Built to be easily installed on any budget-friendly hosting solution or free cloud tiers, PHPForm is the perfect choice for
55
developers and businesses looking for a reliable and GDPR-compliant form management solution.
66

7-
<img src="./public/images/screen.png" width="100%">
7+
<img src="./public/images/screen.png" width="100%" alt="Screenshot">
88

99
# Features
1010

Diff for: composer.json

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
{
22
"type": "project",
3-
"license": "proprietary",
3+
"name": "phpform-dev/phpform-server",
4+
"description": "PHPForm - Open Source Headless Form Management Server",
5+
"keywords": [
6+
"phpform",
7+
"form",
8+
"server",
9+
"headless",
10+
"api",
11+
"symfony",
12+
"php"
13+
],
14+
"homepage": "http://phpform.dev",
15+
"license": "MIT",
16+
"version": "0.3.0",
417
"minimum-stability": "stable",
518
"prefer-stable": true,
619
"require": {
@@ -13,6 +26,7 @@
1326
"doctrine/doctrine-bundle": "^2.11",
1427
"doctrine/doctrine-migrations-bundle": "^3.3",
1528
"doctrine/orm": "^2.17",
29+
"guzzlehttp/guzzle": "^7.0",
1630
"minishlink/web-push": "^8.0",
1731
"phpoffice/phpspreadsheet": "^1.29",
1832
"symfony/console": "7.0.*",

Diff for: composer.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/Admin/Controller/FormWebhooksController.php

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
namespace App\Admin\Controller;
3+
4+
use App\Admin\Form\FormWebhookType;
5+
use App\Entity\Form;
6+
use App\Entity\FormWebhook;
7+
use App\Service\FormMenuCounterService;
8+
use App\Service\FormWebhookService;
9+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
10+
use Symfony\Component\HttpFoundation\Request;
11+
use Symfony\Component\HttpFoundation\Response;
12+
use Symfony\Component\Routing\Annotation\Route;
13+
14+
class FormWebhooksController extends AbstractController
15+
{
16+
public function __construct(
17+
private readonly FormMenuCounterService $formMenuCounterService,
18+
private readonly FormWebhookService $formWebhookService,
19+
)
20+
{
21+
}
22+
23+
#[Route('/admin/forms/{id}/webhooks', name: 'admin_forms_webhooks', methods: ['GET', 'POST'])]
24+
public function index(Request $request, Form $formEntity): Response
25+
{
26+
$form = $this->createForm(FormWebhookType::class, new FormWebhook());
27+
$form->handleRequest($request);
28+
29+
if ($form->isSubmitted() && $form->isValid()) {
30+
$webhook = $form->getData();
31+
$webhook->setForm($formEntity);
32+
$this->formWebhookService->save($webhook);
33+
34+
$this->addFlash('primary', 'Webhook added successfully.');
35+
36+
return $this->redirectToRoute('admin_forms_webhooks', ['id' => $formEntity->getId()]);
37+
}
38+
39+
$webhooks = $this->formWebhookService->getAllByFormId($formEntity->getId());
40+
41+
return $this->render('@Admin/form-webhooks/index.html.twig', [
42+
'formEntity' => $formEntity,
43+
'form' => $form->createView(),
44+
'menuCounts' => $this->formMenuCounterService->getAllCountsByFormId($formEntity->getId()),
45+
'webhooks' => $webhooks,
46+
]);
47+
}
48+
49+
#[Route('/admin/forms/{formId}/webhooks/{webhookId}/delete', name: 'admin_forms_webhook_delete', methods: ['GET'])]
50+
public function delete(int $formId, int $webhookId): Response
51+
{
52+
$webhook = $this->formWebhookService->getOneByIdAndFormId($webhookId, $formId);
53+
if (!$webhook) {
54+
throw $this->createNotFoundException('Webhook not found.');
55+
}
56+
57+
$this->formWebhookService->delete($webhook);
58+
59+
$this->addFlash('primary', 'Webhook deleted successfully.');
60+
61+
return $this->redirectToRoute('admin_forms_webhooks', ['id' => $formId]);
62+
}
63+
}

Diff for: src/Admin/Form/FormWebhookHeaderType.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
namespace App\Admin\Form;
3+
4+
use App\Entity\FormWebhookHeader;
5+
use Symfony\Component\Form\AbstractType;
6+
use Symfony\Component\Form\Extension\Core\Type\TextType;
7+
use Symfony\Component\Form\FormBuilderInterface;
8+
use Symfony\Component\OptionsResolver\OptionsResolver;
9+
10+
class FormWebhookHeaderType extends AbstractType
11+
{
12+
public function buildForm(FormBuilderInterface $builder, array $options): void
13+
{
14+
$builder
15+
->add('name', TextType::class, [
16+
'label' => 'Header Name',
17+
'attr' => ['placeholder' => 'Content-Type'],
18+
])
19+
->add('value', TextType::class, [
20+
'label' => 'Header Value',
21+
'attr' => ['placeholder' => 'application/json'],
22+
]);
23+
}
24+
25+
public function configureOptions(OptionsResolver $resolver): void
26+
{
27+
$resolver->setDefaults([
28+
'data_class' => FormWebhookHeader::class,
29+
]);
30+
}
31+
}

Diff for: src/Admin/Form/FormWebhookType.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
namespace App\Admin\Form;
3+
4+
use App\Entity\FormWebhookHeader;
5+
use App\Entity\FormWebhook;
6+
use Symfony\Component\Form\AbstractType;
7+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
8+
use Symfony\Component\Form\Extension\Core\Type\UrlType;
9+
use Symfony\Component\Form\FormBuilderInterface;
10+
use Symfony\Component\OptionsResolver\OptionsResolver;
11+
12+
class FormWebhookType extends AbstractType
13+
{
14+
public function buildForm(FormBuilderInterface $builder, array $options): void
15+
{
16+
$builder
17+
->add('url', UrlType::class, [
18+
'label' => 'Webhook URL',
19+
'attr' => ['placeholder' => 'https://example.com/webhook'],
20+
])
21+
->add('headers', CollectionType::class, [
22+
'entry_type' => FormWebhookHeaderType::class,
23+
'entry_options' => ['label' => false],
24+
'allow_add' => true,
25+
'allow_delete' => true,
26+
'by_reference' => false,
27+
'label' => false,
28+
'prototype' => true,
29+
'attr' => [
30+
'class' => 'header-collection',
31+
],
32+
]);
33+
}
34+
35+
public function configureOptions(OptionsResolver $resolver): void
36+
{
37+
$resolver->setDefaults([
38+
'data_class' => FormWebhook::class,
39+
]);
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{% extends '@Admin/forms/page.html.twig' %}
2+
3+
{% block title %}Webhooks / {{ formEntity.name }} / Forms{% endblock %}
4+
5+
{% block section %}
6+
{% if webhooks|length > 0 %}
7+
<div>
8+
{% for webhook in webhooks %}
9+
<div class="box">
10+
<h6 class="has-text-weight-bold">URL:</h6>
11+
<div>
12+
{{ webhook.url }}
13+
</div>
14+
{% if webhook.headers|length > 0 %}
15+
<div class="mt-2">
16+
<h6 class="has-text-weight-bold mb-2">Headers:</h6>
17+
<div>
18+
{% for header in webhook.headers %}
19+
<div class="mb-2">
20+
<span class="tag is-light">
21+
<strong>{{ header.name }}:</strong> {{ header.value }}
22+
</span>
23+
</div>
24+
{% endfor %}
25+
</div>
26+
</div>
27+
{% endif %}
28+
<div class="mt-3">
29+
<a class="tag is-rounded py-2 has-background-danger-light hoverable" href="{{ path('admin_forms_webhook_delete', { formId: formEntity.id, webhookId: webhook.id }) }}" title="Delete">
30+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="width:16px; height: 16px;"><title/><path d="M21,6a1,1,0,0,1-1,1H4A1,1,0,0,1,4,5H9V4.5A1.5,1.5,0,0,1,10.5,3h3A1.5,1.5,0,0,1,15,4.5V5h5A1,1,0,0,1,21,6Z" fill="#464646"/><path d="M5.5,9v9.5A2.5,2.5,0,0,0,8,21h8a2.5,2.5,0,0,0,2.5-2.5V9ZM11,17a1,1,0,0,1-2,0V13a1,1,0,0,1,2,0Zm4,0a1,1,0,0,1-2,0V13a1,1,0,0,1,2,0Z" fill="#464646"/></svg>
31+
</a>
32+
</div>
33+
</div>
34+
{% endfor %}
35+
</div>
36+
{% endif %}
37+
<div class="card mt-4" x-data="webhookForm()">
38+
<header class="card-header">
39+
<p class="card-header-title">
40+
Add new Webhook
41+
</p>
42+
</header>
43+
<div class="card-content">
44+
<div class="content">
45+
{{ form_start(form, {'attr': {'class': 'form'}}) }}
46+
<div class="field">
47+
{{ form_label(form.url, null, {'label_attr': {'class': 'label'}}) }}
48+
<div class="control">
49+
{{ form_widget(form.url, {'attr': {'class': 'input', 'placeholder': 'Webhook URL'}}) }}
50+
</div>
51+
{{ form_errors(form.url, {'attr': {'class': 'help is-danger'}}) }}
52+
</div>
53+
54+
<div class="is-flex is-align-items-center is-justify-content-space-between mb-2">
55+
<h5>Headers</h5>
56+
<button type="button" class="button is-primary is-inverted" @click="addHeader()">Add Header</button>
57+
</div>
58+
59+
<template x-for="(header, index) in headers" :key="index">
60+
<div class="mb-2 is-flex is-align-items-center">
61+
<div class="mr-3">
62+
<input type="text" :name="'form_webhook[headers][' + index + '][name]'" x-model="header.name" class="input" placeholder="Header Name" required minlength="1" maxlength="100">
63+
</div>
64+
<div class="mr-3">
65+
<input type="text" :name="'form_webhook[headers][' + index + '][value]'" x-model="header.value" class="input" placeholder="Header Value" required minlength="1" maxlength="255">
66+
</div>
67+
<div>
68+
<button type="button" class="button is-danger is-inverted" @click="removeHeader(index)">Remove</button>
69+
</div>
70+
</div>
71+
</template>
72+
73+
<div class="field is-grouped mt-4">
74+
<div class="control">
75+
<button type="submit" class="button is-primary">Add Webhook</button>
76+
</div>
77+
</div>
78+
{{ form_end(form) }}
79+
</div>
80+
</div>
81+
</div>
82+
83+
<script>
84+
function webhookForm() {
85+
return {
86+
headers: [{
87+
name: '',
88+
value: ''
89+
}],
90+
addHeader() {
91+
this.headers.push({ name: '', value: '' });
92+
},
93+
removeHeader(index) {
94+
this.headers.splice(index, 1);
95+
}
96+
};
97+
}
98+
</script>
99+
{% endblock %}

Diff for: src/Admin/Resources/templates/forms/page.html.twig

+27-15
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@
4949
Settings
5050
</p>
5151
<ul class="menu-list">
52+
{% if app.user.isSuperUser %}
53+
<li>
54+
<a
55+
{% if current_path == 'admin_forms_edit' %}
56+
class="is-active"
57+
{% endif %}
58+
href="{{ path('admin_forms_edit', {'id': formEntity.id}) }}"
59+
>
60+
General
61+
</a>
62+
</li>
63+
<li>
64+
<a
65+
{% if current_path starts with 'admin_forms_fields' %}
66+
class="is-active"
67+
{% endif %}
68+
href="{{ path('admin_forms_fields', {'id': formEntity.id}) }}"
69+
>
70+
Fields ({{ menuCounts.fields }})
71+
</a>
72+
</li>
73+
{% endif %}
5274
<li>
5375
<a
5476
{% if current_path == 'admin_forms_info' %}
@@ -70,16 +92,6 @@
7092
</a>
7193
</li>
7294
{% if app.user.isSuperUser %}
73-
<li>
74-
<a
75-
{% if current_path == 'admin_forms_edit' %}
76-
class="is-active"
77-
{% endif %}
78-
href="{{ path('admin_forms_edit', {'id': formEntity.id}) }}"
79-
>
80-
Edit
81-
</a>
82-
</li>
8395
<li>
8496
<a
8597
{% if current_path == 'admin_forms_captcha' %}
@@ -102,12 +114,12 @@
102114
</li>
103115
<li>
104116
<a
105-
{% if current_path starts with 'admin_forms_fields' %}
106-
class="is-active"
107-
{% endif %}
108-
href="{{ path('admin_forms_fields', {'id': formEntity.id}) }}"
117+
{% if current_path == 'admin_forms_webhooks' %}
118+
class="is-active"
119+
{% endif %}
120+
href="{{ path('admin_forms_webhooks', {'id': formEntity.id}) }}"
109121
>
110-
Fields ({{ menuCounts.fields }})
122+
Webhooks ({{ menuCounts.webhooks }})
111123
</a>
112124
</li>
113125
<li>

0 commit comments

Comments
 (0)