PluginProbe
GiveWP – Donation Plugin and Fundraising Platform / 4.18.0
GiveWP – Donation Plugin and Fundraising Platform v4.18.0
4.18.0 4.17.0 4.16.9 4.16.8.1 4.16.8 4.16.7.2 4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 All 257 releases
give / src / Campaigns / Repositories / CampaignsDataRepository.php

CampaignsDataRepository.php in GiveWP – Donation Plugin and Fundraising Platform 4.18.0, at src/Campaigns/Repositories/CampaignsDataRepository.php

273 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Give\Campaigns\Repositories;
4
5 use Give\Campaigns\CampaignsDataQuery;
6 use Give\Campaigns\Models\Campaign;
7 use Give\Campaigns\ValueObjects\CampaignGoalType;
8
9 /**
10 * Used to optimize the campaigns list table performance and to avoid n+1 problems.
11 * Instead of doing expensive queries in multiple columns in each row, this class loads everything upfront for multiple campaigns.
12 *
13 * @since 4.0.0
14 */
15 class CampaignsDataRepository
16 {
17 /**
18 * @var array
19 */
20 private $amounts;
21 /**
22 * @var array
23 */
24 private $subscriptionAmounts = [];
25 /**
26 * @var array
27 */
28 private $donationsCount;
29 /**
30 * @var array
31 */
32 private $subscriptionDonationsCount = [];
33 /**
34 * @var array
35 */
36 private $donorsCount;
37 /**
38 * @var array
39 */
40 private $subscriptionDonorsCount = [];
41
42 /**
43 * Cached stats for the given campaigns. Campaigns missing from the cache are queried and added
44 * to it, so a warm cache never hides a campaign that arrived after it was built.
45 *
46 * The cache holds live-mode stats only. The queries filter by the site's payment mode, and the
47 * options carry no mode, so test mode reads straight from the database and never writes.
48 *
49 * @since 4.18.0 Query and cache campaigns that are missing from the cache; read subscriptions from the option they are written to; bypass the cache in test mode.
50 * @since 4.8.0 added data caching layer
51 *
52 * @param int[] $ids
53 *
54 * @return CampaignsDataRepository
55 */
56 public static function campaigns(array $ids): CampaignsDataRepository
57 {
58 $self = new self();
59 $emptyCache = ['amounts' => [], 'donationsCount' => [], 'donorsCount' => []];
60 $useCache = ! give_is_test_mode();
61 $campaignsData = $useCache
62 ? array_merge($emptyCache, (array)get_option('give_campaigns_data', []))
63 : $emptyCache;
64 $campaignsSubscriptionData = $useCache
65 ? array_merge($emptyCache, (array)get_option('give_campaigns_subscriptions_data', []))
66 : $emptyCache;
67
68 $uncachedIds = self::missingIds($campaignsData, $ids);
69
70 if ($uncachedIds) {
71 $donations = CampaignsDataQuery::donations($uncachedIds);
72
73 $campaignsData = [
74 'amounts' => array_merge($campaignsData['amounts'], self::withZeroRows($donations->collectIntendedAmounts(), $uncachedIds, 'sum')),
75 'donationsCount' => array_merge($campaignsData['donationsCount'], self::withZeroRows($donations->collectDonations(), $uncachedIds, 'count')),
76 'donorsCount' => array_merge($campaignsData['donorsCount'], self::withZeroRows($donations->collectDonors(), $uncachedIds, 'count')),
77 ];
78
79 if ($useCache) {
80 update_option('give_campaigns_data', $campaignsData);
81 }
82 }
83
84 // The subscriptions cache can lag the donations cache, for example when Recurring is activated later
85 $uncachedSubscriptionIds = defined('GIVE_RECURRING_VERSION')
86 ? self::missingIds($campaignsSubscriptionData, $ids)
87 : [];
88
89 if ($uncachedSubscriptionIds) {
90 $subscriptions = CampaignsDataQuery::subscriptions($uncachedSubscriptionIds);
91
92 $campaignsSubscriptionData = [
93 'amounts' => array_merge($campaignsSubscriptionData['amounts'], self::withZeroRows($subscriptions->collectInitialAmounts(), $uncachedSubscriptionIds, 'sum')),
94 'donationsCount' => array_merge($campaignsSubscriptionData['donationsCount'], self::withZeroRows($subscriptions->collectDonations(), $uncachedSubscriptionIds, 'count')),
95 'donorsCount' => array_merge($campaignsSubscriptionData['donorsCount'], self::withZeroRows($subscriptions->collectDonors(), $uncachedSubscriptionIds, 'count')),
96 ];
97
98 if ($useCache) {
99 update_option('give_campaigns_subscriptions_data', $campaignsSubscriptionData);
100 }
101 }
102
103 $self->amounts = $campaignsData['amounts'];
104 $self->donationsCount = $campaignsData['donationsCount'];
105 $self->donorsCount = $campaignsData['donorsCount'];
106
107 if (defined('GIVE_RECURRING_VERSION')) {
108 $self->subscriptionAmounts = $campaignsSubscriptionData['amounts'];
109 $self->subscriptionDonationsCount = $campaignsSubscriptionData['donationsCount'];
110 $self->subscriptionDonorsCount = $campaignsSubscriptionData['donorsCount'];
111 }
112
113 return $self;
114 }
115
116 /**
117 * Ids with no row in the given cache.
118 *
119 * @since 4.18.0
120 */
121 private static function missingIds(array $cache, array $ids): array
122 {
123 $cachedIds = array_map('strval', array_column($cache['donationsCount'], 'campaign_id'));
124
125 return array_values(array_filter($ids, static function ($id) use ($cachedIds) {
126 return ! in_array((string)$id, $cachedIds, true);
127 }));
128 }
129
130 /**
131 * The aggregate queries return no row for a campaign with no donations. Add a zero row for each
132 * such campaign so it counts as cached and is not queried again on every page load.
133 *
134 * @since 4.18.0
135 */
136 private static function withZeroRows($rows, array $ids, string $column): array
137 {
138 $rows = is_array($rows) ? $rows : [];
139 $present = array_map('strval', array_column($rows, 'campaign_id'));
140
141 foreach ($ids as $id) {
142 if ( ! in_array((string)$id, $present, true)) {
143 $rows[] = ['campaign_id' => (string)$id, $column => 0];
144 }
145 }
146
147 return $rows;
148 }
149
150 /**
151 * @since 4.2.0 return type of float
152 * @since 4.0.0
153 *
154 * Get revenue for campaign
155 *
156 * @param Campaign $campaign
157 */
158 public function getRevenue(Campaign $campaign): float
159 {
160 $data = $campaign->goalType->isSubscriptions()
161 ? $this->subscriptionAmounts
162 : $this->amounts;
163
164 foreach ($data as $row) {
165 if (isset($row['campaign_id']) && $row['campaign_id'] == $campaign->id) {
166 return $row['sum'];
167 }
168 }
169
170 return 0;
171 }
172
173 /**
174 * @since 4.0.0
175 *
176 * Get donations count for campaign
177 *
178 * @param Campaign $campaign
179 *
180 * @return int
181 */
182 public function getDonationsCount(Campaign $campaign): int
183 {
184 $data = $campaign->goalType->isSubscriptions()
185 ? $this->subscriptionDonationsCount
186 : $this->donationsCount;
187
188 foreach ($data as $row) {
189 if (isset($row['campaign_id']) && $row['campaign_id'] == $campaign->id) {
190 return (int)$row['count'];
191 }
192 }
193
194 return 0;
195 }
196
197 /**
198 * @since 4.0.0
199 *
200 * Get donors count for campaign
201 *
202 * @param Campaign $campaign
203 *
204 * @return int
205 */
206 public function getDonorsCount(Campaign $campaign): int
207 {
208 $data = $campaign->goalType->isSubscriptions()
209 ? $this->subscriptionDonorsCount
210 : $this->donorsCount;
211
212 foreach ($data as $row) {
213 if (isset($row['campaign_id']) && $row['campaign_id'] == $campaign->id) {
214 return (int)$row['count'];
215 }
216 }
217
218 return 0;
219 }
220
221
222 /**
223 * @since 4.0.0
224 *
225 * Get goal data for campaign
226 *
227 * @param Campaign $campaign
228 *
229 * @return array{actual: int, goal: int, actualFormatted: string, goalFormatted:string, percentage:float}
230 */
231 public function getGoalData(Campaign $campaign): array
232 {
233 $actual = $this->getActualGoal($campaign);
234 $percentage = $campaign->goal
235 ? $actual / $campaign->goal
236 : 0;
237
238 return [
239 'actual' => $actual,
240 'goal' => $campaign->goal,
241 'actualFormatted' => $campaign->goalType == CampaignGoalType::AMOUNT
242 ? give_currency_filter(give_format_amount($actual))
243 : $actual,
244 'goalFormatted' => $campaign->goalType == CampaignGoalType::AMOUNT
245 ? give_currency_filter(give_format_amount($campaign->goal))
246 : $campaign->goal,
247 'percentage' => round($percentage * 100, 2),
248 ];
249 }
250
251 /**
252 * @since 4.2.0 return union type int|float
253 * @since 4.0.0
254 *
255 * @param Campaign $campaign
256 *
257 * @return int|float
258 */
259 private function getActualGoal(Campaign $campaign)
260 {
261 switch ($campaign->goalType->getValue()) {
262 case CampaignGoalType::DONATIONS():
263 case CampaignGoalType::SUBSCRIPTIONS():
264 return $this->getDonationsCount($campaign);
265 case CampaignGoalType::DONORS():
266 case CampaignGoalType::DONORS_FROM_SUBSCRIPTIONS():
267 return $this->getDonorsCount($campaign);
268 default:
269 return $this->getRevenue($campaign);
270 }
271 }
272 }
273