PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.3
MailPoet – Newsletters, Email Marketing, and Automation v5.34.3
5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / CustomFields / CustomFieldsRepository.php
mailpoet / lib / CustomFields Last commit date
RestApi 1 month ago ApiDataSanitizer.php 3 months ago CustomFieldsRepository.php 1 month ago index.php 3 years ago
CustomFieldsRepository.php
542 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\CustomFields;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Doctrine\Repository;
9 use MailPoet\Entities\CustomFieldEntity;
10 use MailPoet\Entities\DynamicSegmentFilterEntity;
11 use MailPoet\Entities\FormEntity;
12 use MailPoet\Entities\SegmentEntity;
13 use MailPoet\Entities\SubscriberCustomFieldEntity;
14 use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields;
15 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
16 use MailPoetVendor\Doctrine\ORM\EntityManager;
17
18 /**
19 * @extends Repository<CustomFieldEntity>
20 */
21 class CustomFieldsRepository extends Repository {
22 public function __construct(
23 EntityManager $entityManager
24 ) {
25 parent::__construct($entityManager);
26 }
27
28 protected function getEntityClassName() {
29 return CustomFieldEntity::class;
30 }
31
32 /**
33 * @param array $data
34 * @return CustomFieldEntity
35 *
36 * Updates the entity in place and does not touch `deletedAt`. Callers that
37 * accept ids from outside (REST endpoints, etc.) must reject trashed fields
38 * before calling this — otherwise the field stays trashed despite the update.
39 */
40 public function createOrUpdate($data) {
41 // set name as label by default
42 if (empty($data['params']['label']) && isset($data['name'])) {
43 $data['params']['label'] = $data['name'];
44 }
45
46 if (isset($data['id'])) {
47 $field = $this->findOneById((int)$data['id']);
48 } elseif (isset($data['name'])) {
49 $field = $this->findOneBy(['name' => $data['name']]);
50 }
51 if (!isset($field)) {
52 $field = new CustomFieldEntity();
53 $this->entityManager->persist($field);
54 }
55 if (isset($data['name'])) $field->setName($data['name']);
56 if (isset($data['type'])) $field->setType($data['type']);
57 if (isset($data['params'])) $field->setParams($data['params']);
58 $this->entityManager->flush();
59 return $field;
60 }
61
62 public function findAllAsArray() {
63 $customFieldsTable = $this->entityManager->getClassMetadata(CustomFieldEntity::class)->getTableName();
64
65 $query = $this->entityManager
66 ->getConnection()
67 ->createQueryBuilder()
68 ->select('*')
69 ->from($customFieldsTable)
70 ->where('deleted_at IS NULL')
71 ->execute();
72
73 return $query->fetchAllAssociative();
74 }
75
76 /**
77 * @return CustomFieldEntity[]
78 */
79 public function findAllActive(): array {
80 return $this->findBy(['deletedAt' => null], ['createdAt' => 'asc']);
81 }
82
83 public function deleteCustomField(CustomFieldEntity $customField): void {
84 $this->bulkTrash([(int)$customField->getId()]);
85 }
86
87 public function hasSubscriberValues(int $customFieldId): bool {
88 $count = (int)$this->entityManager->createQueryBuilder()
89 ->select('COUNT(scf.id)')
90 ->from(SubscriberCustomFieldEntity::class, 'scf')
91 ->where('scf.customField = :id')
92 ->setParameter('id', $customFieldId)
93 ->setMaxResults(1)
94 ->getQuery()->getSingleScalarResult();
95 return $count > 0;
96 }
97
98 /**
99 * @param array<int|null> $ids
100 */
101 public function bulkTrash(array $ids): int {
102 $ids = $this->normalizeIds($ids);
103 if (!$ids) {
104 return 0;
105 }
106
107 $result = $this->entityManager->createQueryBuilder()
108 ->update(CustomFieldEntity::class, 'cf')
109 ->set('cf.deletedAt', 'CURRENT_TIMESTAMP()')
110 ->where('cf.id IN (:ids)')
111 ->andWhere('cf.deletedAt IS NULL')
112 ->setParameter('ids', $ids)
113 ->getQuery()->execute();
114
115 $this->refreshAll(function (CustomFieldEntity $entity) use ($ids) {
116 return in_array($entity->getId(), $ids, true);
117 });
118 return $result;
119 }
120
121 /**
122 * @param array<int|null> $ids
123 */
124 public function bulkRestore(array $ids): int {
125 $ids = $this->normalizeIds($ids);
126 if (!$ids) {
127 return 0;
128 }
129
130 $result = $this->entityManager->createQueryBuilder()
131 ->update(CustomFieldEntity::class, 'cf')
132 ->set('cf.deletedAt', ':deletedAt')
133 ->where('cf.id IN (:ids)')
134 ->andWhere('cf.deletedAt IS NOT NULL')
135 ->setParameter('deletedAt', null)
136 ->setParameter('ids', $ids)
137 ->getQuery()->execute();
138
139 $this->refreshAll(function (CustomFieldEntity $entity) use ($ids) {
140 return in_array($entity->getId(), $ids, true);
141 });
142 return $result;
143 }
144
145 /**
146 * Permanently deletes trashed custom fields and removes references from
147 * subscriber values, forms, and dynamic segments.
148 *
149 * @param array<int|null> $ids
150 * @return int Number of custom fields deleted.
151 */
152 public function bulkDelete(array $ids): int {
153 $ids = $this->normalizeIds($ids);
154 if (!$ids) {
155 return 0;
156 }
157 $ids = $this->findTrashedIds($ids);
158 return $this->deleteTrashedByIds($ids);
159 }
160
161 public function emptyTrash(): int {
162 return $this->deleteTrashedByIds($this->findTrashedIds());
163 }
164
165 /**
166 * @param int[] $ids
167 */
168 private function deleteTrashedByIds(array $ids): int {
169 if (!$ids) {
170 return 0;
171 }
172 $deleted = 0;
173 $this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, &$deleted): void {
174 $this->removeCustomFieldsFromForms($ids);
175 $this->removeCustomFieldsFromDynamicSegments($ids);
176
177 $subscriberCustomFieldTable = $entityManager->getClassMetadata(SubscriberCustomFieldEntity::class)->getTableName();
178 $entityManager->getConnection()->executeStatement(
179 "DELETE FROM $subscriberCustomFieldTable WHERE custom_field_id IN (:ids)",
180 ['ids' => $ids],
181 ['ids' => ArrayParameterType::INTEGER]
182 );
183
184 $customFieldsTable = $entityManager->getClassMetadata(CustomFieldEntity::class)->getTableName();
185 $deleted = (int)$entityManager->getConnection()->executeStatement(
186 "DELETE FROM $customFieldsTable WHERE id IN (:ids)",
187 ['ids' => $ids],
188 ['ids' => ArrayParameterType::INTEGER]
189 );
190 });
191
192 $this->entityManager->clear(CustomFieldEntity::class);
193 $this->entityManager->clear(SubscriberCustomFieldEntity::class);
194 $this->entityManager->clear(FormEntity::class);
195 $this->entityManager->clear(DynamicSegmentFilterEntity::class);
196 return $deleted;
197 }
198
199 /**
200 * Listing with subscriber counts + search + type filter + group + sort +
201 * pagination, all done in SQL. `forms_count` and `dynamic_segments_count` are
202 * derived from JSON (form body blocks, segment filter data) and cannot be
203 * expressed in SQL, so they are computed in PHP for the current page only and
204 * are display-only — they are intentionally not filterable or sortable.
205 *
206 * @param array{search?: string, orderby?: string, order?: string, page?: int, per_page?: int, group?: string, filter?: array{type?: string[]}} $args
207 * @return array{items: array<int, array{id: int, name: string, label: string, type: string, params: array, required: bool, subscribers_count: int, forms_count: int, dynamic_segments_count: int, created_at: ?\DateTimeInterface, updated_at: ?\DateTimeInterface, deleted_at: ?\DateTimeInterface}>, total: int, groups: array<int, array{name: string, label: string, count: int}>}
208 */
209 public function listWithCounts(array $args = []): array {
210 $search = isset($args['search']) ? trim((string)$args['search']) : '';
211 $orderby = isset($args['orderby']) && is_string($args['orderby']) ? $args['orderby'] : 'name';
212 $order = isset($args['order']) && strtolower((string)$args['order']) === 'desc' ? 'DESC' : 'ASC';
213 $page = isset($args['page']) ? max(1, (int)$args['page']) : 1;
214 $perPage = isset($args['per_page']) ? max(1, min(100, (int)$args['per_page'])) : 25;
215 $group = isset($args['group']) && $args['group'] === 'trash' ? 'trash' : 'all';
216 $filter = isset($args['filter']) && is_array($args['filter']) ? $args['filter'] : [];
217 $types = isset($filter['type']) && is_array($filter['type']) ? array_values(array_filter(array_map('strval', $filter['type']))) : [];
218
219 $sortable = [
220 'name' => 'cf.name',
221 'type' => 'cf.type',
222 'created_at' => 'cf.createdAt',
223 'subscribers_count' => 'subscribersCount',
224 ];
225 $orderByExpr = $sortable[$orderby] ?? $sortable['name'];
226
227 $qb = $this->entityManager->createQueryBuilder()
228 ->select('cf.id AS id, cf.name AS name, cf.type AS type, cf.params AS params, cf.createdAt AS created_at, cf.updatedAt AS updated_at, cf.deletedAt AS deleted_at, COUNT(DISTINCT s.id) AS subscribersCount')
229 ->from(CustomFieldEntity::class, 'cf')
230 ->leftJoin(SubscriberCustomFieldEntity::class, 'scf', 'WITH', 'scf.customField = cf')
231 ->leftJoin('scf.subscriber', 's', 'WITH', 's.deletedAt IS NULL')
232 ->groupBy('cf.id')
233 ->orderBy($orderByExpr, $order);
234
235 // Deterministic secondary ordering so paginated results are stable on ties.
236 if ($orderby !== 'name') {
237 $qb->addOrderBy('cf.name', 'ASC');
238 }
239 $qb->addOrderBy('cf.id', 'ASC')
240 ->setFirstResult(($page - 1) * $perPage)
241 ->setMaxResults($perPage);
242
243 $this->applyListFilters($qb, $search, $types, $group);
244
245 /** @var array<array{id: int, name: string, type: string, params: mixed, created_at: mixed, updated_at: mixed, deleted_at: mixed, subscribersCount: int|string}> $rows */
246 $rows = $qb->getQuery()->getArrayResult();
247
248 $groups = $this->getGroups();
249 $total = $this->countWithFilters($search, $types, $group);
250
251 $customFieldIds = array_map('intval', array_column($rows, 'id'));
252 $formsCounts = $this->getFormCountsByCustomFieldIds($customFieldIds);
253 $dynamicSegmentsCounts = $this->getDynamicSegmentCountsByCustomFieldIds($customFieldIds);
254
255 $items = [];
256 foreach ($rows as $row) {
257 $id = (int)$row['id'];
258 $params = is_array($row['params']) ? $row['params'] : [];
259 $label = isset($params['label']) && is_scalar($params['label']) ? (string)$params['label'] : (string)$row['name'];
260 $createdAt = $row['created_at'] ?? null;
261 $updatedAt = $row['updated_at'] ?? null;
262 $deletedAt = $row['deleted_at'] ?? null;
263
264 $items[] = [
265 'id' => $id,
266 'name' => (string)$row['name'],
267 'label' => $label,
268 'type' => (string)$row['type'],
269 'params' => $params,
270 'required' => (bool)($params['required'] ?? false),
271 'subscribers_count' => (int)$row['subscribersCount'],
272 'forms_count' => $formsCounts[$id] ?? 0,
273 'dynamic_segments_count' => $dynamicSegmentsCounts[$id] ?? 0,
274 'created_at' => $createdAt instanceof \DateTimeInterface ? $createdAt : null,
275 'updated_at' => $updatedAt instanceof \DateTimeInterface ? $updatedAt : null,
276 'deleted_at' => $deletedAt instanceof \DateTimeInterface ? $deletedAt : null,
277 ];
278 }
279
280 return ['items' => $items, 'total' => $total, 'groups' => $groups];
281 }
282
283 /**
284 * @param string[] $types
285 */
286 private function applyListFilters(\MailPoetVendor\Doctrine\ORM\QueryBuilder $qb, string $search, array $types, string $group): void {
287 if ($search !== '') {
288 $qb->andWhere('cf.name LIKE :search')
289 ->setParameter('search', '%' . $search . '%');
290 }
291 if ($types) {
292 $qb->andWhere('cf.type IN (:types)')
293 ->setParameter('types', $types);
294 }
295 $this->applyGroup($qb, $group);
296 }
297
298 /**
299 * @param string[] $types
300 */
301 private function countWithFilters(string $search, array $types, string $group): int {
302 $qb = $this->entityManager->createQueryBuilder()
303 ->select('COUNT(cf.id)')
304 ->from(CustomFieldEntity::class, 'cf');
305 $this->applyListFilters($qb, $search, $types, $group);
306 return (int)$qb->getQuery()->getSingleScalarResult();
307 }
308
309 /**
310 * @return array<int, array{name: string, label: string, count: int}>
311 */
312 private function getGroups(): array {
313 $activeCount = $this->countByDeletedAt(false);
314 $trashedCount = $this->countByDeletedAt(true);
315 return [
316 [
317 'name' => 'all',
318 'label' => __('All', 'mailpoet'),
319 'count' => $activeCount,
320 ],
321 [
322 'name' => 'trash',
323 'label' => __('Trash', 'mailpoet'),
324 'count' => $trashedCount,
325 ],
326 ];
327 }
328
329 private function countByDeletedAt(bool $trashed): int {
330 $queryBuilder = $this->entityManager->createQueryBuilder()
331 ->select('COUNT(cf.id)')
332 ->from(CustomFieldEntity::class, 'cf');
333 $this->applyGroup($queryBuilder, $trashed ? 'trash' : 'all');
334 return (int)$queryBuilder->getQuery()->getSingleScalarResult();
335 }
336
337 private function applyGroup(\MailPoetVendor\Doctrine\ORM\QueryBuilder $queryBuilder, string $group): void {
338 if ($group === 'trash') {
339 $queryBuilder->andWhere('cf.deletedAt IS NOT NULL');
340 } else {
341 $queryBuilder->andWhere('cf.deletedAt IS NULL');
342 }
343 }
344
345 /**
346 * @param array<int|null> $ids
347 * @return int[]
348 */
349 private function normalizeIds(array $ids): array {
350 return array_values(array_filter(array_map(
351 static function ($id): int {
352 return (int)$id;
353 },
354 $ids
355 )));
356 }
357
358 /**
359 * @param int[]|null $ids
360 * @return int[]
361 */
362 private function findTrashedIds(?array $ids = null): array {
363 $customFieldsTable = $this->entityManager->getClassMetadata(CustomFieldEntity::class)->getTableName();
364 $queryBuilder = $this->entityManager->getConnection()
365 ->createQueryBuilder()
366 ->select('id')
367 ->from($customFieldsTable)
368 ->where('deleted_at IS NOT NULL');
369 if ($ids !== null) {
370 $queryBuilder
371 ->andWhere('id IN (:ids)')
372 ->setParameter('ids', $ids, ArrayParameterType::INTEGER);
373 }
374 $rows = $queryBuilder->executeQuery()->fetchAllAssociative();
375 $trashedIds = [];
376 foreach ($rows as $row) {
377 $id = $row['id'] ?? null;
378 if (is_int($id) || is_string($id)) {
379 $trashedIds[] = (int)$id;
380 }
381 }
382 return $trashedIds;
383 }
384
385 /**
386 * @param int[] $customFieldIds
387 * @return array<int, int>
388 */
389 private function getFormCountsByCustomFieldIds(array $customFieldIds): array {
390 if (!$customFieldIds) {
391 return [];
392 }
393
394 $customFieldIdsLookup = array_flip($customFieldIds);
395 $counts = array_fill_keys($customFieldIds, 0);
396 /** @var FormEntity[] $forms */
397 $forms = $this->entityManager->createQueryBuilder()
398 ->select('f')
399 ->from(FormEntity::class, 'f')
400 ->where('f.deletedAt IS NULL')
401 ->getQuery()
402 ->getResult();
403
404 foreach ($forms as $form) {
405 $formCustomFieldIds = [];
406 foreach ($form->getBlocksByTypes(FormEntity::FORM_FIELD_TYPES) as $block) {
407 $customFieldId = isset($block['id']) ? (int)$block['id'] : 0;
408 if (isset($customFieldIdsLookup[$customFieldId])) {
409 $formCustomFieldIds[$customFieldId] = true;
410 }
411 }
412 foreach (array_keys($formCustomFieldIds) as $customFieldId) {
413 $counts[$customFieldId]++;
414 }
415 }
416
417 return $counts;
418 }
419
420 /**
421 * @param int[] $customFieldIds
422 */
423 private function removeCustomFieldsFromForms(array $customFieldIds): void {
424 $customFieldIdsLookup = array_flip($customFieldIds);
425 /** @var FormEntity[] $forms */
426 $forms = $this->entityManager->createQueryBuilder()
427 ->select('f')
428 ->from(FormEntity::class, 'f')
429 ->getQuery()
430 ->getResult();
431
432 foreach ($forms as $form) {
433 $body = $form->getBody();
434 if (!is_array($body)) {
435 continue;
436 }
437 $updatedBody = $this->removeCustomFieldBlocks($body, $customFieldIdsLookup);
438 if ($updatedBody !== $body) {
439 $form->setBody($updatedBody);
440 }
441 }
442 }
443
444 /**
445 * @param array<mixed, mixed> $blocks
446 * @param array<int, int> $customFieldIdsLookup
447 * @return array<int, mixed>
448 */
449 private function removeCustomFieldBlocks(array $blocks, array $customFieldIdsLookup): array {
450 $updated = [];
451 foreach ($blocks as $block) {
452 if (!is_array($block)) {
453 $updated[] = $block;
454 continue;
455 }
456
457 $rawCustomFieldId = $block['id'] ?? null;
458 $customFieldId = is_int($rawCustomFieldId) || is_string($rawCustomFieldId) ? (int)$rawCustomFieldId : 0;
459 $type = isset($block['type']) && is_string($block['type']) ? $block['type'] : null;
460 if ($type !== null && in_array($type, FormEntity::FORM_FIELD_TYPES, true) && isset($customFieldIdsLookup[$customFieldId])) {
461 continue;
462 }
463
464 if (isset($block['body']) && is_array($block['body'])) {
465 $block['body'] = $this->removeCustomFieldBlocks($block['body'], $customFieldIdsLookup);
466 }
467 $updated[] = $block;
468 }
469 return $updated;
470 }
471
472 /**
473 * @param int[] $customFieldIds
474 */
475 private function removeCustomFieldsFromDynamicSegments(array $customFieldIds): void {
476 $customFieldIdsLookup = array_flip($customFieldIds);
477 /** @var DynamicSegmentFilterEntity[] $filters */
478 $filters = $this->entityManager->createQueryBuilder()
479 ->select('dsf')
480 ->from(DynamicSegmentFilterEntity::class, 'dsf')
481 ->where('dsf.filterData.action = :action')
482 ->setParameter('action', MailPoetCustomFields::TYPE)
483 ->getQuery()
484 ->getResult();
485
486 foreach ($filters as $filter) {
487 $customFieldIdParam = $filter->getFilterData()->getParam('custom_field_id');
488 if (!is_int($customFieldIdParam) && !is_string($customFieldIdParam)) {
489 continue;
490 }
491 if (isset($customFieldIdsLookup[(int)$customFieldIdParam])) {
492 $this->entityManager->remove($filter);
493 }
494 }
495 }
496
497 /**
498 * @param int[] $customFieldIds
499 * @return array<int, int>
500 */
501 private function getDynamicSegmentCountsByCustomFieldIds(array $customFieldIds): array {
502 if (!$customFieldIds) {
503 return [];
504 }
505
506 $customFieldIdsLookup = array_flip($customFieldIds);
507 $segmentIdsByCustomFieldId = array_fill_keys($customFieldIds, []);
508 /** @var DynamicSegmentFilterEntity[] $filters */
509 $filters = $this->entityManager->createQueryBuilder()
510 ->select('dsf')
511 ->from(DynamicSegmentFilterEntity::class, 'dsf')
512 ->join('dsf.segment', 's')
513 ->where('s.deletedAt IS NULL')
514 ->andWhere('dsf.filterData.action = :action')
515 ->setParameter('action', MailPoetCustomFields::TYPE)
516 ->getQuery()
517 ->getResult();
518
519 foreach ($filters as $filter) {
520 $customFieldIdParam = $filter->getFilterData()->getParam('custom_field_id');
521 if (!is_int($customFieldIdParam) && !is_string($customFieldIdParam)) {
522 continue;
523 }
524 $customFieldId = (int)$customFieldIdParam;
525 if (!isset($customFieldIdsLookup[$customFieldId])) {
526 continue;
527 }
528 $segment = $filter->getSegment();
529 if (!$segment instanceof SegmentEntity) {
530 continue;
531 }
532 $segmentIdsByCustomFieldId[$customFieldId][(int)$segment->getId()] = true;
533 }
534
535 $counts = [];
536 foreach ($segmentIdsByCustomFieldId as $customFieldId => $segmentIds) {
537 $counts[$customFieldId] = count($segmentIds);
538 }
539 return $counts;
540 }
541 }
542