Compare commits
10 Commits
cf3a2ddc14
...
980f656a0a
| Author | SHA1 | Date | |
|---|---|---|---|
| 980f656a0a | |||
| c0ca0c731d | |||
| 0295a4b28b | |||
| 91c5d56903 | |||
| 172c56de7d | |||
| e6f570e1c3 | |||
|
|
b5a46c9123 | ||
|
|
66790768d9 | ||
|
|
37fe0cabda | ||
|
|
06c1bf2abb |
@@ -4,6 +4,11 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
<!--- END HEADER -->
|
<!--- END HEADER -->
|
||||||
|
|
||||||
|
## [2.2.1](https://github.com/telecart-labs/telecart/compare/v2.2.0...v2.2.1) (2026-02-22)
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.2.0](https://github.com/telecart-labs/telecart/compare/v2.1.0...v2.2.0) (2026-01-09)
|
## [2.2.0](https://github.com/telecart-labs/telecart/compare/v2.1.0...v2.2.0) (2026-01-09)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
907
frontend/admin/package-lock.json
generated
907
frontend/admin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
|||||||
"@primeuix/themes": "^1.2.5",
|
"@primeuix/themes": "^1.2.5",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.5",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"daisyui": "^5.4.2",
|
"daisyui": "^5.4.2",
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
|
|||||||
48
frontend/admin/src/components/CronExpressionSelect.vue
Normal file
48
frontend/admin/src/components/CronExpressionSelect.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<Dropdown
|
||||||
|
:model-value="modelValue"
|
||||||
|
:options="options"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
placeholder="Интервал"
|
||||||
|
class="tw:w-[14rem] tw:shrink-0"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event ?? '')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Dropdown from 'primevue/dropdown';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
/** Пресеты интервалов (label — для отображения, value — cron expression) */
|
||||||
|
const PRESETS = [
|
||||||
|
{ label: 'Раз в минуту', value: '* * * * *' },
|
||||||
|
{ label: 'Раз в 5 минут', value: '*/5 * * * *' },
|
||||||
|
{ label: 'Раз в 10 минут', value: '*/10 * * * *' },
|
||||||
|
{ label: 'Раз в час', value: '0 * * * *' },
|
||||||
|
{ label: 'Раз в 3 часа', value: '0 */3 * * *' },
|
||||||
|
{ label: 'Раз в 6 часов', value: '0 */6 * * *' },
|
||||||
|
{ label: 'Раз в сутки', value: '0 0 * * *' },
|
||||||
|
{ label: 'Раз в неделю', value: '0 0 * * 0' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const presetValues = new Set(PRESETS.map((p) => p.value));
|
||||||
|
|
||||||
|
/** Только пресеты; если текущее значение не из списка — показываем его в списке (уже сохранённое в БД), чтобы не терять отображение */
|
||||||
|
const options = computed(() => {
|
||||||
|
const current = props.modelValue ?? '';
|
||||||
|
if (!current || presetValues.has(current)) {
|
||||||
|
return PRESETS;
|
||||||
|
}
|
||||||
|
return [{ label: current, value: current }, ...PRESETS];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
104
frontend/admin/src/components/CronJobOrgUrlField.vue
Normal file
104
frontend/admin/src/components/CronJobOrgUrlField.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsItem label="URL для cron-job.org">
|
||||||
|
<template #default>
|
||||||
|
<InputGroup>
|
||||||
|
<Button
|
||||||
|
icon="fa fa-refresh"
|
||||||
|
severity="secondary"
|
||||||
|
:loading="regeneratingUrl"
|
||||||
|
v-tooltip.top="'Перегенерировать URL'"
|
||||||
|
@click="confirmRegenerateUrl"
|
||||||
|
/>
|
||||||
|
<Button icon="fa fa-copy" severity="secondary" @click="copyToClipboard"/>
|
||||||
|
<InputText readonly :model-value="cronJobOrgUrl" class="tw:w-full"/>
|
||||||
|
</InputGroup>
|
||||||
|
</template>
|
||||||
|
<template #help>
|
||||||
|
Создайте задачу на <a href="https://cron-job.org/" target="_blank" rel="noopener" class="tw:underline">cron-job.org</a>, укажите этот URL и интервал (например, каждые 5 минут). Метод: GET. Учитывайте лимиты по времени запроса на вашем хостинге — для тяжёлых задач возможны таймауты. При утечке URL нажмите «Перегенерировать URL» и обновите задачу на cron-job.org.
|
||||||
|
</template>
|
||||||
|
</SettingsItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.js';
|
||||||
|
import SettingsItem from '@/components/SettingsItem.vue';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import InputGroup from 'primevue/inputgroup';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
import { toastBus } from '@/utils/toastHelper.js';
|
||||||
|
import { apiPost } from '@/utils/http.js';
|
||||||
|
|
||||||
|
const settings = useSettingsStore();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const regeneratingUrl = ref(false);
|
||||||
|
|
||||||
|
const cronJobOrgUrl = computed(() => settings.items.cron?.schedule_url ?? '');
|
||||||
|
|
||||||
|
function confirmRegenerateUrl(event) {
|
||||||
|
confirm.require({
|
||||||
|
group: 'popup',
|
||||||
|
target: event.currentTarget,
|
||||||
|
message: 'После смены URL его нужно будет обновить в задаче на cron-job.org. Продолжить?',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
rejectProps: { label: 'Отмена', severity: 'secondary', outlined: true },
|
||||||
|
acceptProps: { label: 'Перегенерировать', severity: 'secondary' },
|
||||||
|
accept: () => regenerateUrl(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateUrl() {
|
||||||
|
regeneratingUrl.value = true;
|
||||||
|
try {
|
||||||
|
const res = await apiPost('regenerateCronScheduleUrl', {});
|
||||||
|
if (res?.success && res.data?.api_key) {
|
||||||
|
settings.items.cron.api_key = res.data.api_key;
|
||||||
|
if (res.data.schedule_url !== undefined) {
|
||||||
|
settings.items.cron.schedule_url = res.data.schedule_url;
|
||||||
|
}
|
||||||
|
toastBus.emit('show', {
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'URL обновлён',
|
||||||
|
detail: 'Обновите URL в задаче на cron-job.org',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toastBus.emit('show', {
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: res?.data?.error ?? 'Не удалось перегенерировать URL',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toastBus.emit('show', {
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: err?.response?.data?.error ?? 'Не удалось перегенерировать URL',
|
||||||
|
life: 4000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
regeneratingUrl.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyToClipboard() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(cronJobOrgUrl.value);
|
||||||
|
toastBus.emit('show', {
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Скопировано',
|
||||||
|
detail: 'URL скопирован в буфер обмена',
|
||||||
|
life: 2000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toastBus.emit('show', {
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Ошибка',
|
||||||
|
detail: 'Не удалось скопировать текст',
|
||||||
|
life: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
108
frontend/admin/src/components/ScheduledJobsList.vue
Normal file
108
frontend/admin/src/components/ScheduledJobsList.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tw:border tw:border-gray-300 tw:rounded-md tw:bg-white tw:overflow-hidden">
|
||||||
|
<div class="tw:flex tw:flex-col">
|
||||||
|
<div
|
||||||
|
v-for="job in scheduledJobs"
|
||||||
|
:key="job.id"
|
||||||
|
class="tw:flex tw:items-center tw:gap-4 tw:py-3 tw:px-3 tw:border-b tw:border-gray-200 tw:last:border-b-0"
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
:model-value="Boolean(job.is_enabled)"
|
||||||
|
@update:model-value="onJobToggle(job, $event)"
|
||||||
|
/>
|
||||||
|
<div class="tw:min-w-0 tw:flex-1 tw:flex tw:flex-col tw:gap-0.5">
|
||||||
|
<span class="tw:text-[inherit] tw:font-bold">{{ hasJobMeta(job.name) ? jobMeta(job.name).friendlyName : job.name }}</span>
|
||||||
|
<span v-if="hasJobMeta(job.name) && jobMeta(job.name).description" class="tw:text-sm tw:text-gray-500">
|
||||||
|
{{ jobMeta(job.name).description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="job.failed_reason"
|
||||||
|
class="tw:inline-flex tw:items-center tw:gap-1 tw:shrink-0 tw:px-2 tw:py-1 tw:rounded tw:text-sm tw:bg-red-100 tw:text-red-700 tw:cursor-default"
|
||||||
|
role="img"
|
||||||
|
:aria-label="'Ошибка: ' + job.failed_reason"
|
||||||
|
v-tooltip.top="errorTooltip(job)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-exclamation-circle" aria-hidden="true"/>
|
||||||
|
Ошибка
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="tw:flex tw:flex-col tw:gap-0.5 tw:shrink-0 tw:items-end"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="tw:inline-flex tw:items-center tw:shrink-0 tw:px-2 tw:py-1 tw:rounded tw:text-sm tw:bg-green-100 tw:text-green-800 tw:cursor-default"
|
||||||
|
v-tooltip.top="'Дата последнего успешного запуска'"
|
||||||
|
>
|
||||||
|
{{ formatLastRun(job.last_success_at) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="formatDuration(job.last_duration_seconds)"
|
||||||
|
class="tw:text-xs tw:text-gray-500"
|
||||||
|
>
|
||||||
|
Время выполнения: {{ formatDuration(job.last_duration_seconds) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="tw:shrink-0 tw:w-[14rem]">
|
||||||
|
<CronExpressionSelect
|
||||||
|
:model-value="job.cron_expression"
|
||||||
|
@update:model-value="job.cron_expression = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!scheduledJobs.length" class="tw:text-gray-500 tw:py-4 tw:px-3">
|
||||||
|
Нет запланированных задач
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.js';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
|
import CronExpressionSelect from '@/components/CronExpressionSelect.vue';
|
||||||
|
|
||||||
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
|
/** Человекочитаемое имя и описание задач (ключ — job.name с бэкенда) */
|
||||||
|
const JOB_META = {
|
||||||
|
telecart_pulse_send_events: {
|
||||||
|
friendlyName: 'Отправка данных в TeleCart Pulse',
|
||||||
|
description: 'Отправка данных телеметрии о действиях в TeleCart. Требуется для сбора метрик по рассылкам и кампаниям, сделанных через сервис TeleCart Pulse',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function hasJobMeta(jobName) {
|
||||||
|
return jobName in JOB_META;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jobMeta(jobName) {
|
||||||
|
return JOB_META[jobName];
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledJobs = computed(() => settings.items.scheduled_jobs ?? []);
|
||||||
|
|
||||||
|
function formatLastRun(value) {
|
||||||
|
if (!value) return '—';
|
||||||
|
const d = typeof value === 'string' ? new Date(value.replace(' ', 'T')) : new Date(value);
|
||||||
|
return Number.isNaN(d.getTime()) ? value : d.toLocaleString('ru-RU');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
if (seconds == null || seconds === '' || Number(seconds) <= 0) return '';
|
||||||
|
const n = Number(seconds);
|
||||||
|
if (Number.isNaN(n)) return '';
|
||||||
|
if (n < 1) return 'менее 1с';
|
||||||
|
return `${n % 1 ? n.toFixed(1) : Math.round(n)}с`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorTooltip(job) {
|
||||||
|
const dateStr = formatLastRun(job.failed_at);
|
||||||
|
return `${dateStr}\n${job.failed_reason ?? ''}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onJobToggle(job, isEnabled) {
|
||||||
|
job.is_enabled = isEnabled ? 1 : 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<SettingsItem :label="label">
|
<SettingsItem :label="label">
|
||||||
<template #default>
|
<template #default>
|
||||||
|
<InputGroup v-if="allowCopy && isSupported">
|
||||||
|
<Button
|
||||||
|
:key="copied ? 'copied' : 'copy'"
|
||||||
|
:icon="copied ? 'fa fa-check' : 'fa fa-copy'"
|
||||||
|
severity="secondary"
|
||||||
|
v-tooltip.top="{ value: copied ? 'Скопировано' : 'Скопировать' }"
|
||||||
|
@click="copyToClipboard"
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
:type="type"
|
||||||
|
v-model="model"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:readonly="readonly"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
|
v-else
|
||||||
:type="type"
|
:type="type"
|
||||||
v-model="model"
|
v-model="model"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -18,6 +36,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import SettingsItem from "@/components/SettingsItem.vue";
|
import SettingsItem from "@/components/SettingsItem.vue";
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
|
import InputGroup from 'primevue/inputgroup';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import { useClipboard } from '@vueuse/core';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
@@ -36,10 +57,16 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
allowCopy: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const model = defineModel();
|
const model = defineModel();
|
||||||
|
|
||||||
|
const { copy, copied, isSupported } = useClipboard({ source: model })
|
||||||
|
|
||||||
|
function copyToClipboard() {
|
||||||
|
copy();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:readonly="true"
|
:readonly="true"
|
||||||
:modelValue="model"
|
:modelValue="model"
|
||||||
|
:allowCopy="true"
|
||||||
>
|
>
|
||||||
Токен, полученный при создании бота через @BotFather.
|
Ссылка на сайт с TeleCart витриной, которую нужно указывать в настройках MiniApp в @BotFather.<br>
|
||||||
Он используется для взаимодействия модуля с Telegram API.
|
Подробная инструкция по настройке в
|
||||||
Подробная инструкция доступна в
|
<a href="https://docs.telecart.pro/telegram/telegram/" target="_blank">
|
||||||
<a href="https://nikitakiselev.github.io/telecart-docs/#telegram" target="_blank">
|
|
||||||
документации <i class="fa fa-external-link"></i>
|
документации <i class="fa fa-external-link"></i>
|
||||||
</a>.
|
</a>.
|
||||||
</ItemInput>
|
</ItemInput>
|
||||||
|
|||||||
@@ -1,24 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label" for="module_tgshop_status">
|
<div class="col-sm-2 tw:flex tw:flex-col tw:gap-1">
|
||||||
{{ label }}
|
<label class="control-label" for="module_tgshop_status">
|
||||||
</label>
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<a
|
||||||
|
v-if="docHref"
|
||||||
|
:href="docHref"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="tw:inline-flex tw:items-center tw:gap-1 tw:text-sm tw:text-gray-500 hover:tw:text-gray-700 tw:underline tw:decoration-dotted tw:decoration-1 tw:underline-offset-2 tw:w-fit tw:self-end"
|
||||||
|
>
|
||||||
|
<i class="fa fa-external-link tw:text-xs" aria-hidden="true"></i>
|
||||||
|
<span>Документация</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<slot name="default"></slot>
|
<slot name="default"></slot>
|
||||||
<div class="help-block">
|
<div class="help-block">
|
||||||
<slot name="help"></slot>
|
<slot name="help"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="hasExpandable">
|
||||||
|
<Button
|
||||||
|
:label="expandableLabel"
|
||||||
|
severity="info"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
/>
|
||||||
|
<div v-show="expanded" class="tw:mt-2 tw:space-y-2">
|
||||||
|
<slot name="expandable"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, useSlots, computed } from 'vue';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
/** Ссылка на документацию: отображается под label, открывается в новой вкладке */
|
||||||
|
docHref: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
/** Подпись кнопки раскрытия блока #expandable (по умолчанию «Подробнее») */
|
||||||
|
expandableLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'Подробнее',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
const hasExpandable = computed(() => !!slots.expandable);
|
||||||
|
const expanded = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ export const useSettingsStore = defineStore('settings', {
|
|||||||
|
|
||||||
cron: {
|
cron: {
|
||||||
mode: 'disabled',
|
mode: 'disabled',
|
||||||
|
api_key: '',
|
||||||
|
schedule_url: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
scheduled_jobs: [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -121,6 +125,14 @@ export const useSettingsStore = defineStore('settings', {
|
|||||||
...this.items,
|
...this.items,
|
||||||
...response.data,
|
...response.data,
|
||||||
};
|
};
|
||||||
|
// Нормализуем типы у запланированных задач (is_enabled — число 0/1), чтобы переключение туда-обратно не меняло хеш
|
||||||
|
const jobs = this.items.scheduled_jobs;
|
||||||
|
if (Array.isArray(jobs)) {
|
||||||
|
this.items.scheduled_jobs = jobs.map((job) => ({
|
||||||
|
...job,
|
||||||
|
is_enabled: job.is_enabled === true || job.is_enabled === 1 || job.is_enabled === '1' ? 1 : 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
// Сохраняем хеш исходного состояния после загрузки
|
// Сохраняем хеш исходного состояния после загрузки
|
||||||
this.originalItemsHash = md5(JSON.stringify(this.items));
|
this.originalItemsHash = md5(JSON.stringify(this.items));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<SettingsItem label="Режим работы планировщика">
|
<SettingsItem label="Режим работы планировщика" doc-href="https://docs.telecart.pro/features/cron/">
|
||||||
<template #default>
|
<template #default>
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="settings.items.cron.mode"
|
v-model="settings.items.cron.mode"
|
||||||
@@ -13,67 +13,106 @@
|
|||||||
<div v-if="settings.items.cron.mode === 'disabled'" class="tw:text-red-600 tw:font-bold">
|
<div v-if="settings.items.cron.mode === 'disabled'" class="tw:text-red-600 tw:font-bold">
|
||||||
Все фоновые задачи отключены.
|
Все фоновые задачи отключены.
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="settings.items.cron.mode === 'cron_job_org'">
|
||||||
|
Задачи запускаются по вызову URL с сервиса <a href="https://cron-job.org/" target="_blank" rel="noopener" class="tw:underline">cron-job.org</a>. Подходит для лёгких задач; при большом количестве товаров или тяжёлых операциях возможны таймауты.
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
Рекомендуемый режим. Использует системный планировщик задач Linux.
|
Рекомендуемый режим. Использует системный планировщик задач Linux.
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<div class="tw:mt-2">
|
<template #expandable>
|
||||||
<p>
|
<p>
|
||||||
<strong>Системный CRON (рекомендуется):</strong> Стабильное выполнение задач по расписанию, независимо от
|
<strong>Системный CRON (рекомендуется):</strong> Задачи выполняются через команду PHP в оболочке сервера (CLI), без HTTP и без ограничений по времени запроса. Не зависит от посещаемости сайта и подходит для любых объёмов данных, в том числе для тяжёлых задач и больших каталогов. Требует доступа к серверу (SSH или панель с CRON). Добавьте команду ниже в планировщик (обычно <code class="tw:px-1 tw:py-0.5 tw:bg-gray-100 tw:dark:bg-gray-800 tw:rounded">crontab -e</code>) для запуска каждые 5 минут.
|
||||||
посещаемости сайта. Добавьте команду в CRON для автоматического выполнения каждые 5 минут.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
<strong>cron-job.org:</strong> Внешний сервис по расписанию вызывает URL вашего сайта по HTTP. Не требует доступа к серверу — удобно для shared-хостинга без CRON. Ограничения: выполнение идёт через веб-запрос, поэтому есть лимиты по времени (timeout у хостинга и у cron-job.org). <strong>Не подходит для тяжёлых сайтов</strong> (много товаров, большие каталоги, тяжёлые задачи): запрос может обрываться по таймауту, задачи не успеют завершиться. Выбирайте этот способ только если нет доступа к системному CRON и нагрузка на планировщик небольшая.
|
||||||
<strong>Выключено:</strong> Все фоновые задачи отключены. Планировщик не будет выполнять никаких задач.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
</div>
|
<strong>Выключено:</strong> Все фоновые задачи отключены. Планировщик не будет выполнять никаких задач.
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem label="Последний запуск CRON">
|
<div class="tw:relative tw:mt-4">
|
||||||
<template #default>
|
<div
|
||||||
<div v-if="lastRunDate" class="tw:text-green-600 tw:font-bold tw:py-2">
|
:class="[
|
||||||
{{ lastRunDate }}
|
'tw:transition-all tw:duration-200',
|
||||||
</div>
|
settings.items.cron.mode === 'disabled'
|
||||||
<div v-else class="tw:text-gray-500 tw:py-2">
|
? 'tw:blur-[2px] tw:pointer-events-none tw:select-none'
|
||||||
Еще не запускался
|
: '',
|
||||||
</div>
|
]"
|
||||||
</template>
|
>
|
||||||
<template #help>
|
<SettingsItem label="Последний запуск CRON">
|
||||||
Время последнего успешного выполнения планировщика задач.
|
<template #default>
|
||||||
</template>
|
<div v-if="lastRunDate" class="tw:text-green-600 tw:font-bold tw:py-2">
|
||||||
</SettingsItem>
|
{{ lastRunDate }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="tw:text-gray-500 tw:py-2">
|
||||||
|
Еще не запускался
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #help>
|
||||||
|
Время последнего успешного выполнения планировщика задач.
|
||||||
|
</template>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
v-if="settings.items.cron.mode === 'system'"
|
v-if="settings.items.cron.mode === 'system'"
|
||||||
label="Команда для CRON"
|
label="Команда для CRON"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Button icon="fa fa-copy" severity="secondary" @click="copyToClipboard(cronCommand)"/>
|
<Button icon="fa fa-copy" severity="secondary" @click="copyToClipboard(cronCommand)"/>
|
||||||
<InputText readonly :model-value="cronCommand" class="tw:w-full"/>
|
<InputText readonly :model-value="cronCommand" class="tw:w-full"/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</template>
|
</template>
|
||||||
<template #help>
|
<template #help>
|
||||||
Добавьте эту строку в конфигурацию CRON на вашем сервере (обычно `crontab -e`), чтобы запускать планировщик каждые
|
Добавьте эту строку в конфигурацию CRON на вашем сервере (обычно `crontab -e`), чтобы запускать планировщик каждые
|
||||||
5 минут.
|
5 минут.
|
||||||
</template>
|
</template>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
<CronJobOrgUrlField v-if="settings.items.cron.mode === 'cron_job_org'"/>
|
||||||
|
|
||||||
|
<SettingsItem label="Задачи планировщика">
|
||||||
|
<template #default>
|
||||||
|
<ScheduledJobsList />
|
||||||
|
</template>
|
||||||
|
<template #help>
|
||||||
|
Включение и отключение задач планировщика. Дата последнего успешного запуска; при ошибке отображается иконка с подсказкой.
|
||||||
|
</template>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="settings.items.cron.mode === 'disabled'"
|
||||||
|
class="tw:absolute tw:inset-0 tw:flex tw:items-center tw:justify-center tw:rounded-lg tw:bg-white/80 tw:dark:bg-gray-900/80 tw:z-10"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span class="tw:text-lg tw:font-semibold tw:text-gray-600 tw:dark:text-gray-400">
|
||||||
|
Планировщик выключен
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {computed} from 'vue';
|
import { computed } from 'vue';
|
||||||
import {useSettingsStore} from "@/stores/settings.js";
|
import { useSettingsStore } from '@/stores/settings.js';
|
||||||
import SettingsItem from "@/components/SettingsItem.vue";
|
import SettingsItem from '@/components/SettingsItem.vue';
|
||||||
import SelectButton from "primevue/selectbutton";
|
import ScheduledJobsList from '@/components/ScheduledJobsList.vue';
|
||||||
import InputText from "primevue/inputtext";
|
import CronJobOrgUrlField from '@/components/CronJobOrgUrlField.vue';
|
||||||
import Button from "primevue/button";
|
import SelectButton from 'primevue/selectbutton';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Button from 'primevue/button';
|
||||||
import InputGroup from 'primevue/inputgroup';
|
import InputGroup from 'primevue/inputgroup';
|
||||||
import {toastBus} from "@/utils/toastHelper.js";
|
import { toastBus } from '@/utils/toastHelper.js';
|
||||||
|
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
const cronModes = [
|
const cronModes = [
|
||||||
{value: 'system', label: 'Системный CRON (Linux)'},
|
{value: 'system', label: 'Системный CRON (Linux)'},
|
||||||
|
{value: 'cron_job_org', label: 'cron-job.org'},
|
||||||
{value: 'disabled', label: 'Выключено'},
|
{value: 'disabled', label: 'Выключено'},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
953
frontend/spa/package-lock.json
generated
953
frontend/spa/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
|||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
"ofetch": "^1.4.1",
|
"ofetch": "^1.4.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.1.2",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ class ControllerExtensionModuleTgshop extends Controller
|
|||||||
'app' => [
|
'app' => [
|
||||||
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
|
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
|
||||||
'language_id' => (int) $this->config->get('config_language_id'),
|
'language_id' => (int) $this->config->get('config_language_id'),
|
||||||
|
'oc_timezone' => $this->config->get('config_timezone'),
|
||||||
],
|
],
|
||||||
'paths' => [
|
'paths' => [
|
||||||
'images' => DIR_IMAGE,
|
'images' => DIR_IMAGE,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use Console\ApplicationFactory;
|
|||||||
use Console\Commands\CacheClearCommand;
|
use Console\Commands\CacheClearCommand;
|
||||||
use Console\Commands\CustomerCountsCommand;
|
use Console\Commands\CustomerCountsCommand;
|
||||||
use Console\Commands\PulseSendEventsCommand;
|
use Console\Commands\PulseSendEventsCommand;
|
||||||
use Console\Commands\ScheduleListCommand;
|
|
||||||
use Console\Commands\ScheduleRunCommand;
|
use Console\Commands\ScheduleRunCommand;
|
||||||
use Console\Commands\VersionCommand;
|
use Console\Commands\VersionCommand;
|
||||||
use Console\Commands\ImagesWarmupCacheCommand;
|
use Console\Commands\ImagesWarmupCacheCommand;
|
||||||
@@ -21,7 +20,7 @@ if (PHP_SAPI !== 'cli') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$baseDir = __DIR__;
|
$baseDir = __DIR__;
|
||||||
$debug = true;
|
$debug = false;
|
||||||
|
|
||||||
if (is_readable($baseDir . '/oc_telegram_shop.phar')) {
|
if (is_readable($baseDir . '/oc_telegram_shop.phar')) {
|
||||||
require_once "phar://{$baseDir}/oc_telegram_shop.phar/vendor/autoload.php";
|
require_once "phar://{$baseDir}/oc_telegram_shop.phar/vendor/autoload.php";
|
||||||
@@ -33,8 +32,6 @@ if (is_readable($baseDir . '/oc_telegram_shop.phar')) {
|
|||||||
throw new RuntimeException('Unable to locate application directory.');
|
throw new RuntimeException('Unable to locate application directory.');
|
||||||
}
|
}
|
||||||
|
|
||||||
date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
// Get Settings from Database
|
// Get Settings from Database
|
||||||
$host = DB_HOSTNAME;
|
$host = DB_HOSTNAME;
|
||||||
$username = DB_USERNAME;
|
$username = DB_USERNAME;
|
||||||
@@ -46,11 +43,14 @@ $dsn = "mysql:host=$host;port=$port;dbname=$dbName";
|
|||||||
$pdo = new PDO($dsn, $username, $password);
|
$pdo = new PDO($dsn, $username, $password);
|
||||||
$connection = new MySqlConnection($pdo);
|
$connection = new MySqlConnection($pdo);
|
||||||
$raw = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'module_telecart_settings'");
|
$raw = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'module_telecart_settings'");
|
||||||
|
$timezone = $connection->select("SELECT value FROM `{$prefix}setting` WHERE `key` = 'config_timezone'");
|
||||||
|
$timezone = $timezone[0]['value'] ?? 'UTC';
|
||||||
$json = json_decode($raw[0]['value'], true, 512, JSON_THROW_ON_ERROR);
|
$json = json_decode($raw[0]['value'], true, 512, JSON_THROW_ON_ERROR);
|
||||||
$items = Arr::mergeArraysRecursively($json, [
|
$items = Arr::mergeArraysRecursively($json, [
|
||||||
'app' => [
|
'app' => [
|
||||||
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
|
'shop_base_url' => HTTPS_CATALOG, // for catalog: HTTPS_SERVER, for admin: HTTPS_CATALOG
|
||||||
'language_id' => 1,
|
'language_id' => 1,
|
||||||
|
'oc_timezone' => $timezone,
|
||||||
],
|
],
|
||||||
'paths' => [
|
'paths' => [
|
||||||
'images' => DIR_IMAGE,
|
'images' => DIR_IMAGE,
|
||||||
@@ -96,7 +96,6 @@ $app->boot();
|
|||||||
$console = new Application('TeleCart', module_version());
|
$console = new Application('TeleCart', module_version());
|
||||||
$console->add($app->get(VersionCommand::class));
|
$console->add($app->get(VersionCommand::class));
|
||||||
$console->add($app->get(ScheduleRunCommand::class));
|
$console->add($app->get(ScheduleRunCommand::class));
|
||||||
$console->add($app->get(ScheduleListCommand::class));
|
|
||||||
$console->add($app->get(PulseSendEventsCommand::class));
|
$console->add($app->get(PulseSendEventsCommand::class));
|
||||||
$console->add($app->get(ImagesWarmupCacheCommand::class));
|
$console->add($app->get(ImagesWarmupCacheCommand::class));
|
||||||
$console->add($app->get(ImagesCacheClearCommand::class));
|
$console->add($app->get(ImagesCacheClearCommand::class));
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ TELECART_CACHE_DRIVER=redis
|
|||||||
#TELECART_REDIS_HOST=redis
|
#TELECART_REDIS_HOST=redis
|
||||||
#TELECART_REDIS_PORT=6379
|
#TELECART_REDIS_PORT=6379
|
||||||
#TELECART_REDIS_DATABASE=0
|
#TELECART_REDIS_DATABASE=0
|
||||||
|
|
||||||
|
SENTRY_ENABLED=false
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_ENABLE_LOGS=false
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ TELECART_CACHE_DRIVER=mysql
|
|||||||
TELECART_REDIS_HOST=redis
|
TELECART_REDIS_HOST=redis
|
||||||
TELECART_REDIS_PORT=6379
|
TELECART_REDIS_PORT=6379
|
||||||
TELECART_REDIS_DATABASE=0
|
TELECART_REDIS_DATABASE=0
|
||||||
|
|
||||||
|
SENTRY_ENABLED=false
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_ENABLE_LOGS=false
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ namespace Bastion\Handlers;
|
|||||||
|
|
||||||
use Bastion\Exceptions\BotTokenConfiguratorException;
|
use Bastion\Exceptions\BotTokenConfiguratorException;
|
||||||
use Bastion\Services\BotTokenConfigurator;
|
use Bastion\Services\BotTokenConfigurator;
|
||||||
|
use Bastion\Services\CronApiKeyRegenerator;
|
||||||
use Bastion\Services\SettingsService;
|
use Bastion\Services\SettingsService;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||||
use Openguru\OpenCartFramework\Config\Settings;
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
@@ -13,35 +15,56 @@ use Openguru\OpenCartFramework\Http\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
|
||||||
|
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class SettingsHandler
|
class SettingsHandler
|
||||||
{
|
{
|
||||||
private BotTokenConfigurator $botTokenConfigurator;
|
private BotTokenConfigurator $botTokenConfigurator;
|
||||||
|
private CronApiKeyRegenerator $cronApiKeyRegenerator;
|
||||||
private Settings $settings;
|
private Settings $settings;
|
||||||
private SettingsService $settingsUpdateService;
|
private SettingsService $settingsUpdateService;
|
||||||
private CacheInterface $cache;
|
private CacheInterface $cache;
|
||||||
private LoggerInterface $logger;
|
private LoggerInterface $logger;
|
||||||
private Builder $builder;
|
private Builder $builder;
|
||||||
private ConnectionInterface $connection;
|
private ConnectionInterface $connection;
|
||||||
|
private ScheduledJob $scheduledJob;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
BotTokenConfigurator $botTokenConfigurator,
|
BotTokenConfigurator $botTokenConfigurator,
|
||||||
|
CronApiKeyRegenerator $cronApiKeyRegenerator,
|
||||||
Settings $settings,
|
Settings $settings,
|
||||||
SettingsService $settingsUpdateService,
|
SettingsService $settingsUpdateService,
|
||||||
CacheInterface $cache,
|
CacheInterface $cache,
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
Builder $builder,
|
Builder $builder,
|
||||||
ConnectionInterface $connection
|
ConnectionInterface $connection,
|
||||||
|
ScheduledJob $scheduledJob
|
||||||
) {
|
) {
|
||||||
$this->botTokenConfigurator = $botTokenConfigurator;
|
$this->botTokenConfigurator = $botTokenConfigurator;
|
||||||
|
$this->cronApiKeyRegenerator = $cronApiKeyRegenerator;
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
$this->settingsUpdateService = $settingsUpdateService;
|
$this->settingsUpdateService = $settingsUpdateService;
|
||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->builder = $builder;
|
$this->builder = $builder;
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
|
$this->scheduledJob = $scheduledJob;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перегенерировать секретный ключ в URL для cron-job.org (сохраняет cron.api_key).
|
||||||
|
*/
|
||||||
|
public function regenerateCronScheduleUrl(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$newApiKey = $this->cronApiKeyRegenerator->regenerate();
|
||||||
|
$scheduleUrl = $this->buildCronScheduleUrl(
|
||||||
|
$this->settings->get('app.shop_base_url', ''),
|
||||||
|
$newApiKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse(['api_key' => $newApiKey, 'schedule_url' => $scheduleUrl]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureBotToken(Request $request): JsonResponse
|
public function configureBotToken(Request $request): JsonResponse
|
||||||
@@ -81,6 +104,12 @@ class SettingsHandler
|
|||||||
// Add CRON system details (read-only)
|
// Add CRON system details (read-only)
|
||||||
$data['cron']['cli_path'] = BP_REAL_BASE_PATH . '/cli.php';
|
$data['cron']['cli_path'] = BP_REAL_BASE_PATH . '/cli.php';
|
||||||
$data['cron']['last_run'] = $this->getLastCronRunDate();
|
$data['cron']['last_run'] = $this->getLastCronRunDate();
|
||||||
|
$data['cron']['schedule_url'] = $this->buildCronScheduleUrl(
|
||||||
|
$this->settings->get('app.shop_base_url', ''),
|
||||||
|
$this->settings->get('cron.api_key', '')
|
||||||
|
);
|
||||||
|
|
||||||
|
$data['scheduled_jobs'] = $this->scheduledJob->all();
|
||||||
|
|
||||||
$forms = $this->builder->newQuery()
|
$forms = $this->builder->newQuery()
|
||||||
->from('telecart_forms')
|
->from('telecart_forms')
|
||||||
@@ -106,6 +135,21 @@ class SettingsHandler
|
|||||||
return new JsonResponse(compact('data'));
|
return new JsonResponse(compact('data'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildCronScheduleUrl(string $shopBaseUrl, string $apiKey): string
|
||||||
|
{
|
||||||
|
$base = rtrim($shopBaseUrl, '/');
|
||||||
|
if ($base === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$params = http_build_query([
|
||||||
|
'route' => 'extension/tgshop/handle',
|
||||||
|
'api_action' => 'runSchedule',
|
||||||
|
'api_key' => $apiKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $base . '/index.php?' . $params;
|
||||||
|
}
|
||||||
|
|
||||||
public function saveSettingsForm(Request $request): JsonResponse
|
public function saveSettingsForm(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$input = $request->json();
|
$input = $request->json();
|
||||||
@@ -116,6 +160,7 @@ class SettingsHandler
|
|||||||
if (isset($input['cron'])) {
|
if (isset($input['cron'])) {
|
||||||
unset($input['cron']['cli_path']);
|
unset($input['cron']['cli_path']);
|
||||||
unset($input['cron']['last_run']);
|
unset($input['cron']['last_run']);
|
||||||
|
unset($input['cron']['schedule_url']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->settingsUpdateService->update(
|
$this->settingsUpdateService->update(
|
||||||
@@ -146,6 +191,25 @@ class SettingsHandler
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update scheduled jobs is_enabled and cron_expression
|
||||||
|
$scheduledJobs = Arr::get($input, 'scheduled_jobs', []);
|
||||||
|
foreach ($scheduledJobs as $job) {
|
||||||
|
$id = (int) ($job['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$isEnabled = filter_var($job['is_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
if ($isEnabled) {
|
||||||
|
$this->scheduledJob->enable($id);
|
||||||
|
} else {
|
||||||
|
$this->scheduledJob->disable($id);
|
||||||
|
}
|
||||||
|
$cronExpression = trim((string) ($job['cron_expression'] ?? ''));
|
||||||
|
if ($cronExpression !== '') {
|
||||||
|
$this->scheduledJob->updateCronExpression($id, $cronExpression);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +238,7 @@ class SettingsHandler
|
|||||||
$lastRunTimestamp = $this->cache->get("scheduler.global_last_run");
|
$lastRunTimestamp = $this->cache->get("scheduler.global_last_run");
|
||||||
|
|
||||||
if ($lastRunTimestamp) {
|
if ($lastRunTimestamp) {
|
||||||
return date('d.m.Y H:i:s', (int)$lastRunTimestamp);
|
return Carbon::createFromTimestamp($lastRunTimestamp)->toDateTimeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Bastion\Services;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
|
|
||||||
|
class CronApiKeyRegenerator
|
||||||
|
{
|
||||||
|
private Settings $settings;
|
||||||
|
private SettingsService $settingsUpdateService;
|
||||||
|
|
||||||
|
public function __construct(Settings $settings, SettingsService $settingsUpdateService)
|
||||||
|
{
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->settingsUpdateService = $settingsUpdateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует новый API-ключ для URL cron-job.org и сохраняет в настройки.
|
||||||
|
*
|
||||||
|
* @return string новый api_key
|
||||||
|
*/
|
||||||
|
public function regenerate(): string
|
||||||
|
{
|
||||||
|
$newApiKey = bin2hex(random_bytes(32));
|
||||||
|
$all = $this->settings->getAll();
|
||||||
|
if (! isset($all['cron'])) {
|
||||||
|
$all['cron'] = [];
|
||||||
|
}
|
||||||
|
$all['cron']['api_key'] = $newApiKey;
|
||||||
|
$this->settingsUpdateService->update($all);
|
||||||
|
|
||||||
|
return $newApiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ return [
|
|||||||
'getSettingsForm' => [SettingsHandler::class, 'getSettingsForm'],
|
'getSettingsForm' => [SettingsHandler::class, 'getSettingsForm'],
|
||||||
'getTelegramCustomers' => [TelegramCustomersHandler::class, 'getCustomers'],
|
'getTelegramCustomers' => [TelegramCustomersHandler::class, 'getCustomers'],
|
||||||
'resetCache' => [SettingsHandler::class, 'resetCache'],
|
'resetCache' => [SettingsHandler::class, 'resetCache'],
|
||||||
|
'regenerateCronScheduleUrl' => [SettingsHandler::class, 'regenerateCronScheduleUrl'],
|
||||||
'saveSettingsForm' => [SettingsHandler::class, 'saveSettingsForm'],
|
'saveSettingsForm' => [SettingsHandler::class, 'saveSettingsForm'],
|
||||||
'getSystemInfo' => [SettingsHandler::class, 'getSystemInfo'],
|
'getSystemInfo' => [SettingsHandler::class, 'getSystemInfo'],
|
||||||
'sendMessageToCustomer' => [SendMessageHandler::class, 'sendMessage'],
|
'sendMessageToCustomer' => [SendMessageHandler::class, 'sendMessage'],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nikitakiselev/oc_telegram_shop",
|
"name": "nikitakiselev/oc_telegram_shop",
|
||||||
"version": "v2.2.0",
|
"version": "v2.2.1",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Openguru\\OpenCartFramework\\": "framework/",
|
"Openguru\\OpenCartFramework\\": "framework/",
|
||||||
@@ -36,7 +36,8 @@
|
|||||||
"ramsey/uuid": "^4.2",
|
"ramsey/uuid": "^4.2",
|
||||||
"symfony/http-foundation": "^5.4",
|
"symfony/http-foundation": "^5.4",
|
||||||
"symfony/console": "^5.4",
|
"symfony/console": "^5.4",
|
||||||
"dragonmantank/cron-expression": "^3.5"
|
"dragonmantank/cron-expression": "^3.5",
|
||||||
|
"sentry/sentry": "^4.19"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/sql-formatter": "^1.3",
|
"doctrine/sql-formatter": "^1.3",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "f6cee85921b310fd27ed8290e977c0ec",
|
"content-hash": "de8b131e728d32534c955f568d8ee53e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -251,29 +251,29 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/deprecations",
|
"name": "doctrine/deprecations",
|
||||||
"version": "1.1.5",
|
"version": "1.1.6",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/doctrine/deprecations.git",
|
"url": "https://github.com/doctrine/deprecations.git",
|
||||||
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
|
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
|
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||||
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
|
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1 || ^8.0"
|
"php": "^7.1 || ^8.0"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"phpunit/phpunit": "<=7.5 || >=13"
|
"phpunit/phpunit": "<=7.5 || >=14"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/coding-standard": "^9 || ^12 || ^13",
|
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||||
"phpstan/phpstan": "1.4.10 || 2.1.11",
|
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
|
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||||
"psr/log": "^1 || ^2 || ^3"
|
"psr/log": "^1 || ^2 || ^3"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
@@ -293,9 +293,9 @@
|
|||||||
"homepage": "https://www.doctrine-project.org/",
|
"homepage": "https://www.doctrine-project.org/",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
|
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||||
},
|
},
|
||||||
"time": "2025-04-07T20:06:18+00:00"
|
"time": "2026-02-07T07:09:04+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "doctrine/event-manager",
|
"name": "doctrine/event-manager",
|
||||||
@@ -455,24 +455,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "graham-campbell/result-type",
|
"name": "graham-campbell/result-type",
|
||||||
"version": "v1.1.3",
|
"version": "v1.1.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/GrahamCampbell/Result-Type.git",
|
"url": "https://github.com/GrahamCampbell/Result-Type.git",
|
||||||
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
|
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
|
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
|
||||||
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
|
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.2.5 || ^8.0",
|
"php": "^7.2.5 || ^8.0",
|
||||||
"phpoption/phpoption": "^1.9.3"
|
"phpoption/phpoption": "^1.9.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
|
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -501,7 +501,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
|
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
|
||||||
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
|
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -513,7 +513,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-07-20T21:45:45+00:00"
|
"time": "2025-12-27T19:43:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "guzzlehttp/guzzle",
|
"name": "guzzlehttp/guzzle",
|
||||||
@@ -926,16 +926,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "2.10.0",
|
"version": "2.11.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Seldaek/monolog.git",
|
"url": "https://github.com/Seldaek/monolog.git",
|
||||||
"reference": "5cf826f2991858b54d5c3809bee745560a1042a7"
|
"reference": "37308608e599f34a1a4845b16440047ec98a172a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7",
|
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a",
|
||||||
"reference": "5cf826f2991858b54d5c3809bee745560a1042a7",
|
"reference": "37308608e599f34a1a4845b16440047ec98a172a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -953,7 +953,7 @@
|
|||||||
"graylog2/gelf-php": "^1.4.2 || ^2@dev",
|
"graylog2/gelf-php": "^1.4.2 || ^2@dev",
|
||||||
"guzzlehttp/guzzle": "^7.4",
|
"guzzlehttp/guzzle": "^7.4",
|
||||||
"guzzlehttp/psr7": "^2.2",
|
"guzzlehttp/psr7": "^2.2",
|
||||||
"mongodb/mongodb": "^1.8",
|
"mongodb/mongodb": "^1.8 || ^2.0",
|
||||||
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
||||||
"phpspec/prophecy": "^1.15",
|
"phpspec/prophecy": "^1.15",
|
||||||
"phpstan/phpstan": "^1.10",
|
"phpstan/phpstan": "^1.10",
|
||||||
@@ -1012,7 +1012,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/Seldaek/monolog/issues",
|
"issues": "https://github.com/Seldaek/monolog/issues",
|
||||||
"source": "https://github.com/Seldaek/monolog/tree/2.10.0"
|
"source": "https://github.com/Seldaek/monolog/tree/2.11.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1024,7 +1024,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-11-12T12:43:37+00:00"
|
"time": "2026-01-01T13:05:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nesbot/carbon",
|
"name": "nesbot/carbon",
|
||||||
@@ -1135,16 +1135,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.4",
|
"version": "1.9.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/schmittjoh/php-option.git",
|
"url": "https://github.com/schmittjoh/php-option.git",
|
||||||
"reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
|
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
|
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
|
||||||
"reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
|
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1194,7 +1194,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/schmittjoh/php-option/issues",
|
"issues": "https://github.com/schmittjoh/php-option/issues",
|
||||||
"source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
|
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1206,7 +1206,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-08-21T11:53:16+00:00"
|
"time": "2025-12-27T19:41:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "predis/predis",
|
"name": "predis/predis",
|
||||||
@@ -3260,26 +3260,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vlucas/phpdotenv",
|
"name": "vlucas/phpdotenv",
|
||||||
"version": "v5.6.2",
|
"version": "v5.6.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/vlucas/phpdotenv.git",
|
"url": "https://github.com/vlucas/phpdotenv.git",
|
||||||
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
|
"reference": "955e7815d677a3eaa7075231212f2110983adecc"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
|
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
|
||||||
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
|
"reference": "955e7815d677a3eaa7075231212f2110983adecc",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-pcre": "*",
|
"ext-pcre": "*",
|
||||||
"graham-campbell/result-type": "^1.1.3",
|
"graham-campbell/result-type": "^1.1.4",
|
||||||
"php": "^7.2.5 || ^8.0",
|
"php": "^7.2.5 || ^8.0",
|
||||||
"phpoption/phpoption": "^1.9.3",
|
"phpoption/phpoption": "^1.9.5",
|
||||||
"symfony/polyfill-ctype": "^1.24",
|
"symfony/polyfill-ctype": "^1.26",
|
||||||
"symfony/polyfill-mbstring": "^1.24",
|
"symfony/polyfill-mbstring": "^1.26",
|
||||||
"symfony/polyfill-php80": "^1.24"
|
"symfony/polyfill-php80": "^1.26"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||||
@@ -3328,7 +3328,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/vlucas/phpdotenv/issues",
|
"issues": "https://github.com/vlucas/phpdotenv/issues",
|
||||||
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
|
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3340,7 +3340,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-04-30T23:37:27+00:00"
|
"time": "2025-12-27T19:49:13+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
@@ -3522,23 +3522,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "marcocesarato/php-conventional-changelog",
|
"name": "marcocesarato/php-conventional-changelog",
|
||||||
"version": "1.17.2",
|
"version": "1.17.3",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/marcocesarato/php-conventional-changelog.git",
|
"url": "https://github.com/marcocesarato/php-conventional-changelog.git",
|
||||||
"reference": "2a153727bab28c85d54184975292a468fac269f7"
|
"reference": "c49b4a69ddf9ecbf055e8029c65e54a956d7ffa2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/marcocesarato/php-conventional-changelog/zipball/2a153727bab28c85d54184975292a468fac269f7",
|
"url": "https://api.github.com/repos/marcocesarato/php-conventional-changelog/zipball/c49b4a69ddf9ecbf055e8029c65e54a956d7ffa2",
|
||||||
"reference": "2a153727bab28c85d54184975292a468fac269f7",
|
"reference": "c49b4a69ddf9ecbf055e8029c65e54a956d7ffa2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"php": ">=7.1.3",
|
"php": ">=7.1.3",
|
||||||
"symfony/console": "^4 || ^5 || ^6 || ^7"
|
"symfony/console": "^4 || ^5 || ^6 || ^7 || ^8"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"brainmaestro/composer-git-hooks": "^2.8",
|
"brainmaestro/composer-git-hooks": "^2.8",
|
||||||
@@ -3594,9 +3594,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/marcocesarato/php-conventional-changelog/issues",
|
"issues": "https://github.com/marcocesarato/php-conventional-changelog/issues",
|
||||||
"source": "https://github.com/marcocesarato/php-conventional-changelog/tree/v1.17.2"
|
"source": "https://github.com/marcocesarato/php-conventional-changelog/tree/v1.17.3"
|
||||||
},
|
},
|
||||||
"time": "2024-05-19T14:35:27+00:00"
|
"time": "2026-01-21T11:40:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mockery/mockery",
|
"name": "mockery/mockery",
|
||||||
@@ -3919,11 +3919,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "2.1.33",
|
"version": "2.1.39",
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224",
|
||||||
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
|
"reference": "c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3968,7 +3968,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-05T10:24:31+00:00"
|
"time": "2026-02-11T14:48:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
@@ -4291,16 +4291,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "9.6.31",
|
"version": "9.6.34",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "945d0b7f346a084ce5549e95289962972c4272e5"
|
"reference": "b36f02317466907a230d3aa1d34467041271ef4a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a",
|
||||||
"reference": "945d0b7f346a084ce5549e95289962972c4272e5",
|
"reference": "b36f02317466907a230d3aa1d34467041271ef4a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -4322,7 +4322,7 @@
|
|||||||
"phpunit/php-timer": "^5.0.3",
|
"phpunit/php-timer": "^5.0.3",
|
||||||
"sebastian/cli-parser": "^1.0.2",
|
"sebastian/cli-parser": "^1.0.2",
|
||||||
"sebastian/code-unit": "^1.0.8",
|
"sebastian/code-unit": "^1.0.8",
|
||||||
"sebastian/comparator": "^4.0.9",
|
"sebastian/comparator": "^4.0.10",
|
||||||
"sebastian/diff": "^4.0.6",
|
"sebastian/diff": "^4.0.6",
|
||||||
"sebastian/environment": "^5.1.5",
|
"sebastian/environment": "^5.1.5",
|
||||||
"sebastian/exporter": "^4.0.8",
|
"sebastian/exporter": "^4.0.8",
|
||||||
@@ -4374,7 +4374,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31"
|
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4398,7 +4398,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-06T07:45:52+00:00"
|
"time": "2026-01-27T05:45:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "roave/security-advisories",
|
"name": "roave/security-advisories",
|
||||||
@@ -4406,12 +4406,12 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
||||||
"reference": "75d4ccd9c135c4ac904cd4211a43e51d12feb1ef"
|
"reference": "92c5ec5685cfbcd7ef721a502e6622516728011c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/75d4ccd9c135c4ac904cd4211a43e51d12feb1ef",
|
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/92c5ec5685cfbcd7ef721a502e6622516728011c",
|
||||||
"reference": "75d4ccd9c135c4ac904cd4211a43e51d12feb1ef",
|
"reference": "92c5ec5685cfbcd7ef721a502e6622516728011c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
@@ -4426,19 +4426,23 @@
|
|||||||
"aimeos/ai-cms-grapesjs": ">=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.9|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.10.8|>=2025.04.1,<2025.10.2",
|
"aimeos/ai-cms-grapesjs": ">=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.9|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.10.8|>=2025.04.1,<2025.10.2",
|
||||||
"aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1",
|
"aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1",
|
||||||
"aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7",
|
"aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7",
|
||||||
|
"aimeos/aimeos-laravel": "==2021.10",
|
||||||
"aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5",
|
"aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5",
|
||||||
"airesvsg/acf-to-rest-api": "<=3.1",
|
"airesvsg/acf-to-rest-api": "<=3.1",
|
||||||
"akaunting/akaunting": "<2.1.13",
|
"akaunting/akaunting": "<2.1.13",
|
||||||
"akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
|
"akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
|
||||||
"alextselegidis/easyappointments": "<1.5.2.0-beta1",
|
"alextselegidis/easyappointments": "<=1.5.2",
|
||||||
"alexusmai/laravel-file-manager": "<=3.3.1",
|
"alexusmai/laravel-file-manager": "<=3.3.1",
|
||||||
|
"algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1",
|
||||||
"alt-design/alt-redirect": "<1.6.4",
|
"alt-design/alt-redirect": "<1.6.4",
|
||||||
|
"altcha-org/altcha": "<1.3.1",
|
||||||
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
|
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
|
||||||
"amazing/media2click": ">=1,<1.3.3",
|
"amazing/media2click": ">=1,<1.3.3",
|
||||||
"ameos/ameos_tarteaucitron": "<1.2.23",
|
"ameos/ameos_tarteaucitron": "<1.2.23",
|
||||||
"amphp/artax": "<1.0.6|>=2,<2.0.6",
|
"amphp/artax": "<1.0.6|>=2,<2.0.6",
|
||||||
"amphp/http": "<=1.7.2|>=2,<=2.1",
|
"amphp/http": "<=1.7.2|>=2,<=2.1",
|
||||||
"amphp/http-client": ">=4,<4.4",
|
"amphp/http-client": ">=4,<4.4",
|
||||||
|
"amphp/http-server": ">=2.0.0.0-RC1-dev,<2.1.10|>=3.0.0.0-beta1,<3.4.4",
|
||||||
"anchorcms/anchor-cms": "<=0.12.7",
|
"anchorcms/anchor-cms": "<=0.12.7",
|
||||||
"andreapollastri/cipi": "<=3.1.15",
|
"andreapollastri/cipi": "<=3.1.15",
|
||||||
"andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5",
|
"andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5",
|
||||||
@@ -4456,22 +4460,22 @@
|
|||||||
"athlon1600/php-proxy-app": "<=3",
|
"athlon1600/php-proxy-app": "<=3",
|
||||||
"athlon1600/youtube-downloader": "<=4",
|
"athlon1600/youtube-downloader": "<=4",
|
||||||
"austintoddj/canvas": "<=3.4.2",
|
"austintoddj/canvas": "<=3.4.2",
|
||||||
"auth0/auth0-php": ">=3.3,<=8.16",
|
"auth0/auth0-php": ">=3.3,<8.18",
|
||||||
"auth0/login": "<=7.18",
|
"auth0/login": "<7.20",
|
||||||
"auth0/symfony": "<=5.4.1",
|
"auth0/symfony": "<=5.5",
|
||||||
"auth0/wordpress": "<=5.3",
|
"auth0/wordpress": "<=5.4",
|
||||||
"automad/automad": "<2.0.0.0-alpha5",
|
"automad/automad": "<2.0.0.0-alpha5",
|
||||||
"automattic/jetpack": "<9.8",
|
"automattic/jetpack": "<9.8",
|
||||||
"awesome-support/awesome-support": "<=6.0.7",
|
"awesome-support/awesome-support": "<=6.0.7",
|
||||||
"aws/aws-sdk-php": "<3.288.1",
|
"aws/aws-sdk-php": "<3.368",
|
||||||
"azuracast/azuracast": "<0.18.3",
|
"azuracast/azuracast": "<=0.23.1",
|
||||||
"b13/seo_basics": "<0.8.2",
|
"b13/seo_basics": "<0.8.2",
|
||||||
"backdrop/backdrop": "<=1.32",
|
"backdrop/backdrop": "<=1.32",
|
||||||
"backpack/crud": "<3.4.9",
|
"backpack/crud": "<3.4.9",
|
||||||
"backpack/filemanager": "<2.0.2|>=3,<3.0.9",
|
"backpack/filemanager": "<2.0.2|>=3,<3.0.9",
|
||||||
"bacula-web/bacula-web": "<9.7.1",
|
"bacula-web/bacula-web": "<9.7.1",
|
||||||
"badaso/core": "<=2.9.11",
|
"badaso/core": "<=2.9.11",
|
||||||
"bagisto/bagisto": "<=2.3.7",
|
"bagisto/bagisto": "<2.3.10",
|
||||||
"barrelstrength/sprout-base-email": "<1.2.7",
|
"barrelstrength/sprout-base-email": "<1.2.7",
|
||||||
"barrelstrength/sprout-forms": "<3.9",
|
"barrelstrength/sprout-forms": "<3.9",
|
||||||
"barryvdh/laravel-translation-manager": "<0.6.8",
|
"barryvdh/laravel-translation-manager": "<0.6.8",
|
||||||
@@ -4503,7 +4507,8 @@
|
|||||||
"bvbmedia/multishop": "<2.0.39",
|
"bvbmedia/multishop": "<2.0.39",
|
||||||
"bytefury/crater": "<6.0.2",
|
"bytefury/crater": "<6.0.2",
|
||||||
"cachethq/cachet": "<2.5.1",
|
"cachethq/cachet": "<2.5.1",
|
||||||
"cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
|
"cadmium-org/cadmium-cms": "<=0.4.9",
|
||||||
|
"cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|>=5.2.10,<5.2.12|==5.3",
|
||||||
"cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
|
"cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
|
||||||
"cardgate/magento2": "<2.0.33",
|
"cardgate/magento2": "<2.0.33",
|
||||||
"cardgate/woocommerce": "<=3.1.15",
|
"cardgate/woocommerce": "<=3.1.15",
|
||||||
@@ -4514,9 +4519,11 @@
|
|||||||
"causal/oidc": "<4",
|
"causal/oidc": "<4",
|
||||||
"cecil/cecil": "<7.47.1",
|
"cecil/cecil": "<7.47.1",
|
||||||
"centreon/centreon": "<22.10.15",
|
"centreon/centreon": "<22.10.15",
|
||||||
|
"cesargb/laravel-magiclink": ">=2,<2.25.1",
|
||||||
"cesnet/simplesamlphp-module-proxystatistics": "<3.1",
|
"cesnet/simplesamlphp-module-proxystatistics": "<3.1",
|
||||||
"chriskacerguis/codeigniter-restserver": "<=2.7.1",
|
"chriskacerguis/codeigniter-restserver": "<=2.7.1",
|
||||||
"chrome-php/chrome": "<1.14",
|
"chrome-php/chrome": "<1.14",
|
||||||
|
"ci4-cms-erp/ci4ms": "<0.28.5",
|
||||||
"civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3",
|
"civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3",
|
||||||
"ckeditor/ckeditor": "<4.25",
|
"ckeditor/ckeditor": "<4.25",
|
||||||
"clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3",
|
"clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3",
|
||||||
@@ -4532,7 +4539,7 @@
|
|||||||
"codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5",
|
"codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5",
|
||||||
"commerceteam/commerce": ">=0.9.6,<0.9.9",
|
"commerceteam/commerce": ">=0.9.6,<0.9.9",
|
||||||
"components/jquery": ">=1.0.3,<3.5",
|
"components/jquery": ">=1.0.3,<3.5",
|
||||||
"composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7",
|
"composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3",
|
||||||
"concrete5/concrete5": "<9.4.3",
|
"concrete5/concrete5": "<9.4.3",
|
||||||
"concrete5/core": "<8.5.8|>=9,<9.1",
|
"concrete5/core": "<8.5.8|>=9,<9.1",
|
||||||
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
|
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
|
||||||
@@ -4542,11 +4549,16 @@
|
|||||||
"contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5",
|
"contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5",
|
||||||
"contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8",
|
"contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8",
|
||||||
"contao/managed-edition": "<=1.5",
|
"contao/managed-edition": "<=1.5",
|
||||||
|
"coreshop/core-shop": "<4.1.9",
|
||||||
"corveda/phpsandbox": "<1.3.5",
|
"corveda/phpsandbox": "<1.3.5",
|
||||||
"cosenary/instagram": "<=2.3",
|
"cosenary/instagram": "<=2.3",
|
||||||
"couleurcitron/tarteaucitron-wp": "<0.3",
|
"couleurcitron/tarteaucitron-wp": "<0.3",
|
||||||
"craftcms/cms": "<=4.16.5|>=5,<=5.8.6",
|
"cpsit/typo3-mailqueue": "<0.4.3|>=0.5,<0.5.1",
|
||||||
"croogo/croogo": "<4",
|
"craftcms/cms": "<4.17.0.0-beta1|>=5,<5.9.0.0-beta1",
|
||||||
|
"craftcms/commerce": ">=4.0.0.0-RC1-dev,<=4.10|>=5,<=5.5.1",
|
||||||
|
"craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1",
|
||||||
|
"craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21",
|
||||||
|
"croogo/croogo": "<=4.0.7",
|
||||||
"cuyz/valinor": "<0.12",
|
"cuyz/valinor": "<0.12",
|
||||||
"czim/file-handling": "<1.5|>=2,<2.3",
|
"czim/file-handling": "<1.5|>=2,<2.3",
|
||||||
"czproject/git-php": "<4.0.3",
|
"czproject/git-php": "<4.0.3",
|
||||||
@@ -4563,10 +4575,11 @@
|
|||||||
"derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4",
|
"derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4",
|
||||||
"desperado/xml-bundle": "<=0.1.7",
|
"desperado/xml-bundle": "<=0.1.7",
|
||||||
"dev-lancer/minecraft-motd-parser": "<=1.0.5",
|
"dev-lancer/minecraft-motd-parser": "<=1.0.5",
|
||||||
"devcode-it/openstamanager": "<=2.9.4",
|
"devcode-it/openstamanager": "<=2.9.8",
|
||||||
"devgroup/dotplant": "<2020.09.14-dev",
|
"devgroup/dotplant": "<2020.09.14-dev",
|
||||||
"digimix/wp-svg-upload": "<=1",
|
"digimix/wp-svg-upload": "<=1",
|
||||||
"directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2",
|
"directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2",
|
||||||
|
"directorytree/imapengine": "<1.22.3",
|
||||||
"dl/yag": "<3.0.1",
|
"dl/yag": "<3.0.1",
|
||||||
"dmk/webkitpdf": "<1.1.4",
|
"dmk/webkitpdf": "<1.1.4",
|
||||||
"dnadesign/silverstripe-elemental": "<5.3.12",
|
"dnadesign/silverstripe-elemental": "<5.3.12",
|
||||||
@@ -4593,7 +4606,7 @@
|
|||||||
"drupal/commerce_alphabank_redirect": "<1.0.3",
|
"drupal/commerce_alphabank_redirect": "<1.0.3",
|
||||||
"drupal/commerce_eurobank_redirect": "<2.1.1",
|
"drupal/commerce_eurobank_redirect": "<2.1.1",
|
||||||
"drupal/config_split": "<1.10|>=2,<2.0.2",
|
"drupal/config_split": "<1.10|>=2,<2.0.2",
|
||||||
"drupal/core": ">=6,<6.38|>=7,<7.102|>=8,<10.4.9|>=10.5,<10.5.6|>=11,<11.1.9|>=11.2,<11.2.8",
|
"drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.4.9|>=10.5,<10.5.6|>=11,<11.1.9|>=11.2,<11.2.8",
|
||||||
"drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
|
"drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
|
||||||
"drupal/currency": "<3.5",
|
"drupal/currency": "<3.5",
|
||||||
"drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
|
"drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
|
||||||
@@ -4623,7 +4636,7 @@
|
|||||||
"ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2",
|
"ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2",
|
||||||
"ecodev/newsletter": "<=4",
|
"ecodev/newsletter": "<=4",
|
||||||
"ectouch/ectouch": "<=2.7.2",
|
"ectouch/ectouch": "<=2.7.2",
|
||||||
"egroupware/egroupware": "<23.1.20240624",
|
"egroupware/egroupware": "<23.1.20260113|>=26.0.20251208,<26.0.20260113",
|
||||||
"elefant/cms": "<2.0.7",
|
"elefant/cms": "<2.0.7",
|
||||||
"elgg/elgg": "<3.3.24|>=4,<4.0.5",
|
"elgg/elgg": "<3.3.24|>=4,<4.0.5",
|
||||||
"elijaa/phpmemcacheadmin": "<=1.3",
|
"elijaa/phpmemcacheadmin": "<=1.3",
|
||||||
@@ -4646,18 +4659,18 @@
|
|||||||
"ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5",
|
"ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5",
|
||||||
"ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12",
|
"ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12",
|
||||||
"ezsystems/ezplatform-http-cache": "<2.3.16",
|
"ezsystems/ezplatform-http-cache": "<2.3.16",
|
||||||
"ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35",
|
"ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<1.3.35",
|
||||||
"ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
|
"ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
|
||||||
"ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40",
|
"ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40",
|
||||||
"ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15",
|
"ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15",
|
||||||
"ezsystems/ezplatform-user": ">=1,<1.0.1",
|
"ezsystems/ezplatform-user": ">=1,<1.0.1",
|
||||||
"ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31",
|
"ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<7.5.31",
|
||||||
"ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1",
|
"ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1",
|
||||||
"ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
|
"ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
|
||||||
"ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15",
|
"ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15",
|
||||||
"ezyang/htmlpurifier": "<=4.2",
|
"ezyang/htmlpurifier": "<=4.2",
|
||||||
"facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2",
|
"facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2",
|
||||||
"facturascripts/facturascripts": "<=2022.08",
|
"facturascripts/facturascripts": "<2025.81",
|
||||||
"fastly/magento2": "<1.2.26",
|
"fastly/magento2": "<1.2.26",
|
||||||
"feehi/cms": "<=2.1.1",
|
"feehi/cms": "<=2.1.1",
|
||||||
"feehi/feehicms": "<=2.1.1",
|
"feehi/feehicms": "<=2.1.1",
|
||||||
@@ -4669,7 +4682,7 @@
|
|||||||
"filegator/filegator": "<7.8",
|
"filegator/filegator": "<7.8",
|
||||||
"filp/whoops": "<2.1.13",
|
"filp/whoops": "<2.1.13",
|
||||||
"fineuploader/php-traditional-server": "<=1.2.2",
|
"fineuploader/php-traditional-server": "<=1.2.2",
|
||||||
"firebase/php-jwt": "<6",
|
"firebase/php-jwt": "<7",
|
||||||
"fisharebest/webtrees": "<=2.1.18",
|
"fisharebest/webtrees": "<=2.1.18",
|
||||||
"fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2",
|
"fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2",
|
||||||
"fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6",
|
"fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6",
|
||||||
@@ -4682,6 +4695,7 @@
|
|||||||
"floriangaerber/magnesium": "<0.3.1",
|
"floriangaerber/magnesium": "<0.3.1",
|
||||||
"fluidtypo3/vhs": "<5.1.1",
|
"fluidtypo3/vhs": "<5.1.1",
|
||||||
"fof/byobu": ">=0.3.0.0-beta2,<1.1.7",
|
"fof/byobu": ">=0.3.0.0-beta2,<1.1.7",
|
||||||
|
"fof/pretty-mail": "<=1.1.2",
|
||||||
"fof/upload": "<1.2.3",
|
"fof/upload": "<1.2.3",
|
||||||
"foodcoopshop/foodcoopshop": ">=3.2,<3.6.1",
|
"foodcoopshop/foodcoopshop": ">=3.2,<3.6.1",
|
||||||
"fooman/tcpdf": "<6.2.22",
|
"fooman/tcpdf": "<6.2.22",
|
||||||
@@ -4697,6 +4711,7 @@
|
|||||||
"friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
|
"friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
|
||||||
"friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6",
|
"friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6",
|
||||||
"froala/wysiwyg-editor": "<=4.3",
|
"froala/wysiwyg-editor": "<=4.3",
|
||||||
|
"frosh/adminer-platform": "<2.2.1",
|
||||||
"froxlor/froxlor": "<=2.2.5",
|
"froxlor/froxlor": "<=2.2.5",
|
||||||
"frozennode/administrator": "<=5.0.12",
|
"frozennode/administrator": "<=5.0.12",
|
||||||
"fuel/core": "<1.8.1",
|
"fuel/core": "<1.8.1",
|
||||||
@@ -4705,9 +4720,9 @@
|
|||||||
"genix/cms": "<=1.1.11",
|
"genix/cms": "<=1.1.11",
|
||||||
"georgringer/news": "<1.3.3",
|
"georgringer/news": "<1.3.3",
|
||||||
"geshi/geshi": "<=1.0.9.1",
|
"geshi/geshi": "<=1.0.9.1",
|
||||||
"getformwork/formwork": "<2.2",
|
"getformwork/formwork": "<=2.3.3",
|
||||||
"getgrav/grav": "<1.11.0.0-beta1",
|
"getgrav/grav": "<1.11.0.0-beta1",
|
||||||
"getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<5.1.4",
|
"getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<=5.2.1",
|
||||||
"getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
|
"getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
|
||||||
"getkirby/panel": "<2.5.14",
|
"getkirby/panel": "<2.5.14",
|
||||||
"getkirby/starterkit": "<=3.7.0.2",
|
"getkirby/starterkit": "<=3.7.0.2",
|
||||||
@@ -4745,9 +4760,9 @@
|
|||||||
"ibexa/http-cache": ">=4.6,<4.6.14",
|
"ibexa/http-cache": ">=4.6,<4.6.14",
|
||||||
"ibexa/post-install": "<1.0.16|>=4.6,<4.6.14",
|
"ibexa/post-install": "<1.0.16|>=4.6,<4.6.14",
|
||||||
"ibexa/solr": ">=4.5,<4.5.4",
|
"ibexa/solr": ">=4.5,<4.5.4",
|
||||||
"ibexa/user": ">=4,<4.4.3|>=5,<5.0.3",
|
"ibexa/user": ">=4,<4.4.3|>=5,<5.0.4",
|
||||||
"icecoder/icecoder": "<=8.1",
|
"icecoder/icecoder": "<=8.1",
|
||||||
"idno/known": "<=1.3.1",
|
"idno/known": "<=1.6.2",
|
||||||
"ilicmiljan/secure-props": ">=1.2,<1.2.2",
|
"ilicmiljan/secure-props": ">=1.2,<1.2.2",
|
||||||
"illuminate/auth": "<5.5.10",
|
"illuminate/auth": "<5.5.10",
|
||||||
"illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4",
|
"illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4",
|
||||||
@@ -4797,7 +4812,7 @@
|
|||||||
"kelvinmo/simplexrd": "<3.1.1",
|
"kelvinmo/simplexrd": "<3.1.1",
|
||||||
"kevinpapst/kimai2": "<1.16.7",
|
"kevinpapst/kimai2": "<1.16.7",
|
||||||
"khodakhah/nodcms": "<=3",
|
"khodakhah/nodcms": "<=3",
|
||||||
"kimai/kimai": "<=2.20.1",
|
"kimai/kimai": "<2.46",
|
||||||
"kitodo/presentation": "<3.2.3|>=3.3,<3.3.4",
|
"kitodo/presentation": "<3.2.3|>=3.3,<3.3.4",
|
||||||
"klaviyo/magento2-extension": ">=1,<3",
|
"klaviyo/magento2-extension": ">=1,<3",
|
||||||
"knplabs/knp-snappy": "<=1.4.2",
|
"knplabs/knp-snappy": "<=1.4.2",
|
||||||
@@ -4816,10 +4831,10 @@
|
|||||||
"laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1",
|
"laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1",
|
||||||
"laravel/laravel": ">=5.4,<5.4.22",
|
"laravel/laravel": ">=5.4,<5.4.22",
|
||||||
"laravel/pulse": "<1.3.1",
|
"laravel/pulse": "<1.3.1",
|
||||||
"laravel/reverb": "<1.4",
|
"laravel/reverb": "<1.7",
|
||||||
"laravel/socialite": ">=1,<2.0.10",
|
"laravel/socialite": ">=1,<2.0.10",
|
||||||
"latte/latte": "<2.10.8",
|
"latte/latte": "<2.10.8",
|
||||||
"lavalite/cms": "<=9|==10.1",
|
"lavalite/cms": "<=10.1",
|
||||||
"lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2",
|
"lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2",
|
||||||
"lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
|
"lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
|
||||||
"league/commonmark": "<2.7",
|
"league/commonmark": "<2.7",
|
||||||
@@ -4828,11 +4843,12 @@
|
|||||||
"leantime/leantime": "<3.3",
|
"leantime/leantime": "<3.3",
|
||||||
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
|
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
|
||||||
"libreform/libreform": ">=2,<=2.0.8",
|
"libreform/libreform": ">=2,<=2.0.8",
|
||||||
"librenms/librenms": "<25.11",
|
"librenms/librenms": "<26.2",
|
||||||
"liftkit/database": "<2.13.2",
|
"liftkit/database": "<2.13.2",
|
||||||
"lightsaml/lightsaml": "<1.3.5",
|
"lightsaml/lightsaml": "<1.3.5",
|
||||||
"limesurvey/limesurvey": "<6.5.12",
|
"limesurvey/limesurvey": "<6.5.12",
|
||||||
"livehelperchat/livehelperchat": "<=3.91",
|
"livehelperchat/livehelperchat": "<=3.91",
|
||||||
|
"livewire-filemanager/filemanager": "<=1.0.4",
|
||||||
"livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4",
|
"livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4",
|
||||||
"livewire/volt": "<1.7",
|
"livewire/volt": "<1.7",
|
||||||
"lms/routes": "<2.1.1",
|
"lms/routes": "<2.1.1",
|
||||||
@@ -4880,8 +4896,9 @@
|
|||||||
"microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1",
|
"microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1",
|
||||||
"microsoft/microsoft-graph-beta": "<2.0.1",
|
"microsoft/microsoft-graph-beta": "<2.0.1",
|
||||||
"microsoft/microsoft-graph-core": "<2.0.2",
|
"microsoft/microsoft-graph-core": "<2.0.2",
|
||||||
"microweber/microweber": "<=2.0.19",
|
"microweber/microweber": "<2.0.20",
|
||||||
"mikehaertl/php-shellcommand": "<1.6.1",
|
"mikehaertl/php-shellcommand": "<1.6.1",
|
||||||
|
"mineadmin/mineadmin": "<=3.0.9",
|
||||||
"miniorange/miniorange-saml": "<1.4.3",
|
"miniorange/miniorange-saml": "<1.4.3",
|
||||||
"mittwald/typo3_forum": "<1.2.1",
|
"mittwald/typo3_forum": "<1.2.1",
|
||||||
"mobiledetect/mobiledetectlib": "<2.8.32",
|
"mobiledetect/mobiledetectlib": "<2.8.32",
|
||||||
@@ -4890,7 +4907,7 @@
|
|||||||
"mongodb/mongodb": ">=1,<1.9.2",
|
"mongodb/mongodb": ">=1,<1.9.2",
|
||||||
"mongodb/mongodb-extension": "<1.21.2",
|
"mongodb/mongodb-extension": "<1.21.2",
|
||||||
"monolog/monolog": ">=1.8,<1.12",
|
"monolog/monolog": ">=1.8,<1.12",
|
||||||
"moodle/moodle": "<4.4.11|>=4.5.0.0-beta,<4.5.7|>=5.0.0.0-beta,<5.0.3",
|
"moodle/moodle": "<4.4.12|>=4.5.0.0-beta,<4.5.8|>=5.0.0.0-beta,<5.0.4|>=5.1.0.0-beta,<5.1.1",
|
||||||
"moonshine/moonshine": "<=3.12.5",
|
"moonshine/moonshine": "<=3.12.5",
|
||||||
"mos/cimage": "<0.7.19",
|
"mos/cimage": "<0.7.19",
|
||||||
"movim/moxl": ">=0.8,<=0.10",
|
"movim/moxl": ">=0.8,<=0.10",
|
||||||
@@ -4935,7 +4952,7 @@
|
|||||||
"october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1",
|
"october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1",
|
||||||
"october/october": "<3.7.5",
|
"october/october": "<3.7.5",
|
||||||
"october/rain": "<1.0.472|>=1.1,<1.1.2",
|
"october/rain": "<1.0.472|>=1.1,<1.1.2",
|
||||||
"october/system": "<3.7.5",
|
"october/system": "<=3.7.12|>=4,<=4.0.11",
|
||||||
"oliverklee/phpunit": "<3.5.15",
|
"oliverklee/phpunit": "<3.5.15",
|
||||||
"omeka/omeka-s": "<4.0.3",
|
"omeka/omeka-s": "<4.0.3",
|
||||||
"onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1",
|
"onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1",
|
||||||
@@ -4943,7 +4960,7 @@
|
|||||||
"open-web-analytics/open-web-analytics": "<1.8.1",
|
"open-web-analytics/open-web-analytics": "<1.8.1",
|
||||||
"opencart/opencart": ">=0",
|
"opencart/opencart": ">=0",
|
||||||
"openid/php-openid": "<2.3",
|
"openid/php-openid": "<2.3",
|
||||||
"openmage/magento-lts": "<20.16",
|
"openmage/magento-lts": "<20.16.1",
|
||||||
"opensolutions/vimbadmin": "<=3.0.15",
|
"opensolutions/vimbadmin": "<=3.0.15",
|
||||||
"opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7",
|
"opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7",
|
||||||
"orchid/platform": ">=8,<14.43",
|
"orchid/platform": ">=8,<14.43",
|
||||||
@@ -4962,6 +4979,7 @@
|
|||||||
"pagekit/pagekit": "<=1.0.18",
|
"pagekit/pagekit": "<=1.0.18",
|
||||||
"paragonie/ecc": "<2.0.1",
|
"paragonie/ecc": "<2.0.1",
|
||||||
"paragonie/random_compat": "<2",
|
"paragonie/random_compat": "<2",
|
||||||
|
"paragonie/sodium_compat": "<1.24|>=2,<2.5",
|
||||||
"passbolt/passbolt_api": "<4.6.2",
|
"passbolt/passbolt_api": "<4.6.2",
|
||||||
"paypal/adaptivepayments-sdk-php": "<=3.9.2",
|
"paypal/adaptivepayments-sdk-php": "<=3.9.2",
|
||||||
"paypal/invoice-sdk-php": "<=3.9",
|
"paypal/invoice-sdk-php": "<=3.9",
|
||||||
@@ -4974,6 +4992,7 @@
|
|||||||
"pear/pear": "<=1.10.1",
|
"pear/pear": "<=1.10.1",
|
||||||
"pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1",
|
"pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1",
|
||||||
"personnummer/personnummer": "<3.0.2",
|
"personnummer/personnummer": "<3.0.2",
|
||||||
|
"ph7software/ph7builder": "<=17.9.1",
|
||||||
"phanan/koel": "<5.1.4",
|
"phanan/koel": "<5.1.4",
|
||||||
"phenx/php-svg-lib": "<0.5.2",
|
"phenx/php-svg-lib": "<0.5.2",
|
||||||
"php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5",
|
"php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5",
|
||||||
@@ -4984,7 +5003,7 @@
|
|||||||
"phpmailer/phpmailer": "<6.5",
|
"phpmailer/phpmailer": "<6.5",
|
||||||
"phpmussel/phpmussel": ">=1,<1.6",
|
"phpmussel/phpmussel": ">=1,<1.6",
|
||||||
"phpmyadmin/phpmyadmin": "<5.2.2",
|
"phpmyadmin/phpmyadmin": "<5.2.2",
|
||||||
"phpmyfaq/phpmyfaq": "<=4.0.13",
|
"phpmyfaq/phpmyfaq": "<=4.0.16",
|
||||||
"phpoffice/common": "<0.2.9",
|
"phpoffice/common": "<0.2.9",
|
||||||
"phpoffice/math": "<=0.2",
|
"phpoffice/math": "<=0.2",
|
||||||
"phpoffice/phpexcel": "<=1.8.2",
|
"phpoffice/phpexcel": "<=1.8.2",
|
||||||
@@ -4993,19 +5012,21 @@
|
|||||||
"phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36",
|
"phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36",
|
||||||
"phpservermon/phpservermon": "<3.6",
|
"phpservermon/phpservermon": "<3.6",
|
||||||
"phpsysinfo/phpsysinfo": "<3.4.3",
|
"phpsysinfo/phpsysinfo": "<3.4.3",
|
||||||
"phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
|
"phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8",
|
||||||
"phpwhois/phpwhois": "<=4.2.5",
|
"phpwhois/phpwhois": "<=4.2.5",
|
||||||
"phpxmlrpc/extras": "<0.6.1",
|
"phpxmlrpc/extras": "<0.6.1",
|
||||||
"phpxmlrpc/phpxmlrpc": "<4.9.2",
|
"phpxmlrpc/phpxmlrpc": "<4.9.2",
|
||||||
|
"phraseanet/phraseanet": "==4.0.3",
|
||||||
"pi/pi": "<=2.5",
|
"pi/pi": "<=2.5",
|
||||||
"pimcore/admin-ui-classic-bundle": "<1.7.6",
|
"pimcore/admin-ui-classic-bundle": "<=1.7.15|>=2.0.0.0-RC1-dev,<=2.2.2",
|
||||||
"pimcore/customer-management-framework-bundle": "<4.2.1",
|
"pimcore/customer-management-framework-bundle": "<4.2.1",
|
||||||
"pimcore/data-hub": "<1.2.4",
|
"pimcore/data-hub": "<1.2.4",
|
||||||
"pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3",
|
"pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3",
|
||||||
"pimcore/demo": "<10.3",
|
"pimcore/demo": "<10.3",
|
||||||
"pimcore/ecommerce-framework-bundle": "<1.0.10",
|
"pimcore/ecommerce-framework-bundle": "<1.0.10",
|
||||||
"pimcore/perspective-editor": "<1.5.1",
|
"pimcore/perspective-editor": "<1.5.1",
|
||||||
"pimcore/pimcore": "<11.5.4",
|
"pimcore/pimcore": "<=11.5.13|>=12.0.0.0-RC1-dev,<12.3.1",
|
||||||
|
"pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1",
|
||||||
"piwik/piwik": "<1.11",
|
"piwik/piwik": "<1.11",
|
||||||
"pixelfed/pixelfed": "<0.12.5",
|
"pixelfed/pixelfed": "<0.12.5",
|
||||||
"plotly/plotly.js": "<2.25.2",
|
"plotly/plotly.js": "<2.25.2",
|
||||||
@@ -5018,7 +5039,7 @@
|
|||||||
"prestashop/blockwishlist": ">=2,<2.1.1",
|
"prestashop/blockwishlist": ">=2,<2.1.1",
|
||||||
"prestashop/contactform": ">=1.0.1,<4.3",
|
"prestashop/contactform": ">=1.0.1,<4.3",
|
||||||
"prestashop/gamification": "<2.3.2",
|
"prestashop/gamification": "<2.3.2",
|
||||||
"prestashop/prestashop": "<8.2.3",
|
"prestashop/prestashop": "<8.2.4|>=9.0.0.0-alpha1,<9.0.3",
|
||||||
"prestashop/productcomments": "<5.0.2",
|
"prestashop/productcomments": "<5.0.2",
|
||||||
"prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5",
|
"prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5",
|
||||||
"prestashop/ps_contactinfo": "<=3.3.2",
|
"prestashop/ps_contactinfo": "<=3.3.2",
|
||||||
@@ -5029,7 +5050,8 @@
|
|||||||
"processwire/processwire": "<=3.0.246",
|
"processwire/processwire": "<=3.0.246",
|
||||||
"propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7",
|
"propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7",
|
||||||
"propel/propel1": ">=1,<=1.7.1",
|
"propel/propel1": ">=1,<=1.7.1",
|
||||||
"pterodactyl/panel": "<=1.11.10",
|
"psy/psysh": "<=0.11.22|>=0.12,<=0.12.18",
|
||||||
|
"pterodactyl/panel": "<1.12.1",
|
||||||
"ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2",
|
"ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2",
|
||||||
"ptrofimov/beanstalk_console": "<1.7.14",
|
"ptrofimov/beanstalk_console": "<1.7.14",
|
||||||
"pubnub/pubnub": "<6.1",
|
"pubnub/pubnub": "<6.1",
|
||||||
@@ -5047,7 +5069,7 @@
|
|||||||
"rap2hpoutre/laravel-log-viewer": "<0.13",
|
"rap2hpoutre/laravel-log-viewer": "<0.13",
|
||||||
"react/http": ">=0.7,<1.9",
|
"react/http": ">=0.7,<1.9",
|
||||||
"really-simple-plugins/complianz-gdpr": "<6.4.2",
|
"really-simple-plugins/complianz-gdpr": "<6.4.2",
|
||||||
"redaxo/source": "<5.20.1",
|
"redaxo/source": "<=5.20.1",
|
||||||
"remdex/livehelperchat": "<4.29",
|
"remdex/livehelperchat": "<4.29",
|
||||||
"renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1",
|
"renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1",
|
||||||
"reportico-web/reportico": "<=8.1",
|
"reportico-web/reportico": "<=8.1",
|
||||||
@@ -5069,10 +5091,10 @@
|
|||||||
"setasign/fpdi": "<2.6.4",
|
"setasign/fpdi": "<2.6.4",
|
||||||
"sfroemken/url_redirect": "<=1.2.1",
|
"sfroemken/url_redirect": "<=1.2.1",
|
||||||
"sheng/yiicms": "<1.2.1",
|
"sheng/yiicms": "<1.2.1",
|
||||||
"shopware/core": "<6.6.10.9-dev|>=6.7,<6.7.4.1-dev",
|
"shopware/core": "<6.6.10.9-dev|>=6.7,<6.7.6.1-dev",
|
||||||
"shopware/platform": "<6.6.10.7-dev|>=6.7,<6.7.3.1-dev",
|
"shopware/platform": "<6.6.10.7-dev|>=6.7,<6.7.3.1-dev",
|
||||||
"shopware/production": "<=6.3.5.2",
|
"shopware/production": "<=6.3.5.2",
|
||||||
"shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.5.1-dev",
|
"shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev",
|
||||||
"shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev",
|
"shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev",
|
||||||
"shopxo/shopxo": "<=6.4",
|
"shopxo/shopxo": "<=6.4",
|
||||||
"showdoc/showdoc": "<2.10.4",
|
"showdoc/showdoc": "<2.10.4",
|
||||||
@@ -5117,7 +5139,7 @@
|
|||||||
"snipe/snipe-it": "<=8.3.4",
|
"snipe/snipe-it": "<=8.3.4",
|
||||||
"socalnick/scn-social-auth": "<1.15.2",
|
"socalnick/scn-social-auth": "<1.15.2",
|
||||||
"socialiteproviders/steam": "<1.1",
|
"socialiteproviders/steam": "<1.1",
|
||||||
"solspace/craft-freeform": ">=5,<5.10.16",
|
"solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6",
|
||||||
"soosyze/soosyze": "<=2",
|
"soosyze/soosyze": "<=2",
|
||||||
"spatie/browsershot": "<5.0.5",
|
"spatie/browsershot": "<5.0.5",
|
||||||
"spatie/image-optimizer": "<1.7.3",
|
"spatie/image-optimizer": "<1.7.3",
|
||||||
@@ -5132,7 +5154,7 @@
|
|||||||
"starcitizentools/short-description": ">=4,<4.0.1",
|
"starcitizentools/short-description": ">=4,<4.0.1",
|
||||||
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1",
|
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1",
|
||||||
"starcitizenwiki/embedvideo": "<=4",
|
"starcitizenwiki/embedvideo": "<=4",
|
||||||
"statamic/cms": "<=5.22",
|
"statamic/cms": "<5.73.9|>=6,<6.3.2",
|
||||||
"stormpath/sdk": "<9.9.99",
|
"stormpath/sdk": "<9.9.99",
|
||||||
"studio-42/elfinder": "<=2.1.64",
|
"studio-42/elfinder": "<=2.1.64",
|
||||||
"studiomitte/friendlycaptcha": "<0.1.4",
|
"studiomitte/friendlycaptcha": "<0.1.4",
|
||||||
@@ -5171,7 +5193,7 @@
|
|||||||
"symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
"symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||||
"symfony/polyfill": ">=1,<1.10",
|
"symfony/polyfill": ">=1,<1.10",
|
||||||
"symfony/polyfill-php55": ">=1,<1.10",
|
"symfony/polyfill-php55": ">=1,<1.10",
|
||||||
"symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7",
|
"symfony/process": "<5.4.51|>=6,<6.4.33|>=7,<7.1.7|>=7.3,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5",
|
||||||
"symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
"symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||||
"symfony/routing": ">=2,<2.0.19",
|
"symfony/routing": ">=2,<2.0.19",
|
||||||
"symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7",
|
"symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7",
|
||||||
@@ -5182,7 +5204,7 @@
|
|||||||
"symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
"symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
||||||
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8",
|
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8",
|
||||||
"symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12",
|
"symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12",
|
||||||
"symfony/symfony": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7",
|
"symfony/symfony": "<5.4.51|>=6,<6.4.33|>=7,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5",
|
||||||
"symfony/translation": ">=2,<2.0.17",
|
"symfony/translation": ">=2,<2.0.17",
|
||||||
"symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8",
|
"symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8",
|
||||||
"symfony/ux-autocomplete": "<2.11.2",
|
"symfony/ux-autocomplete": "<2.11.2",
|
||||||
@@ -5206,7 +5228,7 @@
|
|||||||
"thelia/thelia": ">=2.1,<2.1.3",
|
"thelia/thelia": ">=2.1,<2.1.3",
|
||||||
"theonedemon/phpwhois": "<=4.2.5",
|
"theonedemon/phpwhois": "<=4.2.5",
|
||||||
"thinkcmf/thinkcmf": "<6.0.8",
|
"thinkcmf/thinkcmf": "<6.0.8",
|
||||||
"thorsten/phpmyfaq": "<=4.0.13",
|
"thorsten/phpmyfaq": "<=4.0.16|>=4.1.0.0-alpha,<=4.1.0.0-beta2",
|
||||||
"tikiwiki/tiki-manager": "<=17.1",
|
"tikiwiki/tiki-manager": "<=17.1",
|
||||||
"timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1",
|
"timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1",
|
||||||
"tinymce/tinymce": "<7.2",
|
"tinymce/tinymce": "<7.2",
|
||||||
@@ -5225,10 +5247,10 @@
|
|||||||
"twbs/bootstrap": "<3.4.1|>=4,<4.3.1",
|
"twbs/bootstrap": "<3.4.1|>=4,<4.3.1",
|
||||||
"twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19",
|
"twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19",
|
||||||
"typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2",
|
"typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2",
|
||||||
"typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
"typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
|
||||||
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
|
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
|
||||||
"typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
"typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
||||||
"typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
"typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
|
||||||
"typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
"typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
||||||
"typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1",
|
"typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1",
|
||||||
"typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
|
"typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
|
||||||
@@ -5240,7 +5262,8 @@
|
|||||||
"typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2",
|
"typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2",
|
||||||
"typo3/cms-lowlevel": ">=11,<=11.5.41",
|
"typo3/cms-lowlevel": ">=11,<=11.5.41",
|
||||||
"typo3/cms-recordlist": ">=11,<11.5.48",
|
"typo3/cms-recordlist": ">=11,<11.5.48",
|
||||||
"typo3/cms-recycler": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
|
"typo3/cms-recycler": ">=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
|
||||||
|
"typo3/cms-redirects": ">=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
|
||||||
"typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30",
|
"typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30",
|
||||||
"typo3/cms-scheduler": ">=11,<=11.5.41",
|
"typo3/cms-scheduler": ">=11,<=11.5.41",
|
||||||
"typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11",
|
"typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11",
|
||||||
@@ -5270,7 +5293,7 @@
|
|||||||
"vertexvaar/falsftp": "<0.2.6",
|
"vertexvaar/falsftp": "<0.2.6",
|
||||||
"villagedefrance/opencart-overclocked": "<=1.11.1",
|
"villagedefrance/opencart-overclocked": "<=1.11.1",
|
||||||
"vova07/yii2-fileapi-widget": "<0.1.9",
|
"vova07/yii2-fileapi-widget": "<0.1.9",
|
||||||
"vrana/adminer": "<=4.8.1",
|
"vrana/adminer": "<5.4.2",
|
||||||
"vufind/vufind": ">=2,<9.1.1",
|
"vufind/vufind": ">=2,<9.1.1",
|
||||||
"waldhacker/hcaptcha": "<2.1.2",
|
"waldhacker/hcaptcha": "<2.1.2",
|
||||||
"wallabag/tcpdf": "<6.2.22",
|
"wallabag/tcpdf": "<6.2.22",
|
||||||
@@ -5291,7 +5314,7 @@
|
|||||||
"wikimedia/parsoid": "<0.12.2",
|
"wikimedia/parsoid": "<0.12.2",
|
||||||
"willdurand/js-translation-bundle": "<2.1.1",
|
"willdurand/js-translation-bundle": "<2.1.1",
|
||||||
"winter/wn-backend-module": "<1.2.4",
|
"winter/wn-backend-module": "<1.2.4",
|
||||||
"winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7",
|
"winter/wn-cms-module": "<=1.2.9",
|
||||||
"winter/wn-dusk-plugin": "<2.1",
|
"winter/wn-dusk-plugin": "<2.1",
|
||||||
"winter/wn-system-module": "<1.2.4",
|
"winter/wn-system-module": "<1.2.4",
|
||||||
"wintercms/winter": "<=1.2.3",
|
"wintercms/winter": "<=1.2.3",
|
||||||
@@ -5303,7 +5326,7 @@
|
|||||||
"wpanel/wpanel4-cms": "<=4.3.1",
|
"wpanel/wpanel4-cms": "<=4.3.1",
|
||||||
"wpcloud/wp-stateless": "<3.2",
|
"wpcloud/wp-stateless": "<3.2",
|
||||||
"wpglobus/wpglobus": "<=1.9.6",
|
"wpglobus/wpglobus": "<=1.9.6",
|
||||||
"wwbn/avideo": "<14.3",
|
"wwbn/avideo": "<21",
|
||||||
"xataface/xataface": "<3",
|
"xataface/xataface": "<3",
|
||||||
"xpressengine/xpressengine": "<3.0.15",
|
"xpressengine/xpressengine": "<3.0.15",
|
||||||
"yab/quarx": "<2.4.5",
|
"yab/quarx": "<2.4.5",
|
||||||
@@ -5322,7 +5345,7 @@
|
|||||||
"yiisoft/yii2-redis": "<2.0.20",
|
"yiisoft/yii2-redis": "<2.0.20",
|
||||||
"yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6",
|
"yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6",
|
||||||
"yoast-seo-for-typo3/yoast_seo": "<7.2.3",
|
"yoast-seo-for-typo3/yoast_seo": "<7.2.3",
|
||||||
"yourls/yourls": "<=1.8.2",
|
"yourls/yourls": "<=1.10.2",
|
||||||
"yuan1994/tpadmin": "<=1.3.12",
|
"yuan1994/tpadmin": "<=1.3.12",
|
||||||
"yungifez/skuul": "<=2.6.5",
|
"yungifez/skuul": "<=2.6.5",
|
||||||
"z-push/z-push-dev": "<2.7.6",
|
"z-push/z-push-dev": "<2.7.6",
|
||||||
@@ -5362,7 +5385,8 @@
|
|||||||
"zf-commons/zfc-user": "<1.2.2",
|
"zf-commons/zfc-user": "<1.2.2",
|
||||||
"zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
|
"zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
|
||||||
"zfr/zfr-oauth2-server-module": "<0.1.2",
|
"zfr/zfr-oauth2-server-module": "<0.1.2",
|
||||||
"zoujingli/thinkadmin": "<=6.1.53"
|
"zoujingli/thinkadmin": "<=6.1.53",
|
||||||
|
"zumba/json-serializer": "<3.2.3"
|
||||||
},
|
},
|
||||||
"default-branch": true,
|
"default-branch": true,
|
||||||
"type": "metapackage",
|
"type": "metapackage",
|
||||||
@@ -5400,7 +5424,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-09T18:07:05+00:00"
|
"time": "2026-02-20T22:06:39+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
@@ -5571,16 +5595,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/comparator",
|
"name": "sebastian/comparator",
|
||||||
"version": "4.0.9",
|
"version": "4.0.10",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/comparator.git",
|
"url": "https://github.com/sebastianbergmann/comparator.git",
|
||||||
"reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5"
|
"reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
|
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
|
||||||
"reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
|
"reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -5633,7 +5657,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
"issues": "https://github.com/sebastianbergmann/comparator/issues",
|
||||||
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9"
|
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -5653,7 +5677,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-08-10T06:51:50+00:00"
|
"time": "2026-01-24T09:22:56+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/complexity",
|
"name": "sebastian/complexity",
|
||||||
|
|||||||
@@ -117,5 +117,6 @@ HTML,
|
|||||||
|
|
||||||
'cron' => [
|
'cron' => [
|
||||||
'mode' => 'disabled',
|
'mode' => 'disabled',
|
||||||
|
'api_key' => '',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/** @var \Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry $scheduler */
|
|
||||||
|
|
||||||
// Define your scheduled tasks here.
|
|
||||||
// The $scheduler variable is available in this scope.
|
|
||||||
|
|
||||||
// Example: Running a task class every 5 minutes. Class should have execute method.
|
|
||||||
// $scheduler->add(\My\Task\Class::class)->everyFiveMinutes();
|
|
||||||
|
|
||||||
// Example: Running a closure every hour
|
|
||||||
// $scheduler->add(function () {
|
|
||||||
// // Do something
|
|
||||||
// }, 'my_closure_task')->everyHour();
|
|
||||||
|
|
||||||
// Example: Custom cron expression
|
|
||||||
// $scheduler->add(\My\Task\Class::class)->at('0 12 * * *');
|
|
||||||
|
|
||||||
use Bastion\ScheduledTasks\TeleCartPulseSendEventsTask;
|
|
||||||
|
|
||||||
$scheduler->add(TeleCartPulseSendEventsTask::class, 'telecart_pulse_send_events')->everyTenMinutes();
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Console\Commands;
|
|
||||||
|
|
||||||
use Openguru\OpenCartFramework\Container\Container;
|
|
||||||
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
|
|
||||||
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Helper\Table;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
class ScheduleListCommand extends TeleCartCommand
|
|
||||||
{
|
|
||||||
private SchedulerService $schedulerService;
|
|
||||||
private Container $container;
|
|
||||||
|
|
||||||
protected static $defaultName = 'schedule:list';
|
|
||||||
protected static $defaultDescription = 'List all scheduled tasks and their status';
|
|
||||||
|
|
||||||
public function __construct(SchedulerService $schedulerService, Container $container)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
$this->schedulerService = $schedulerService;
|
|
||||||
$this->container = $container;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$registry = new ScheduleJobRegistry($this->container);
|
|
||||||
|
|
||||||
// Load schedule config
|
|
||||||
$configFile = BP_PHAR_BASE_PATH . '/configs/schedule.php';
|
|
||||||
if (file_exists($configFile)) {
|
|
||||||
$scheduler = $registry; // Variable name used in config file
|
|
||||||
require $configFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
$jobs = $registry->getJobs();
|
|
||||||
$table = new Table($output);
|
|
||||||
$table->setHeaders(['Name / Class', 'Cron Expression', 'Last Run', 'Last Failure']);
|
|
||||||
|
|
||||||
foreach ($jobs as $job) {
|
|
||||||
$id = $job->getId();
|
|
||||||
$lastRun = $this->schedulerService->getLastRun($id);
|
|
||||||
$lastFailure = $this->schedulerService->getLastFailure($id);
|
|
||||||
|
|
||||||
$lastRunText = $lastRun ? date('Y-m-d H:i:s', $lastRun) : 'Never';
|
|
||||||
|
|
||||||
$lastFailureText = 'None';
|
|
||||||
if ($lastFailure) {
|
|
||||||
$lastFailureText = date('Y-m-d H:i:s', $lastFailure['time']) . "\n" . substr($lastFailure['message'], 0, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
$table->addRow([
|
|
||||||
$job->getName(),
|
|
||||||
// We don't have getExpression public yet on Job, assuming we might need to add it or it's not critical.
|
|
||||||
// Wait, Job class stores expression but doesn't expose it via public getter.
|
|
||||||
// I should add getExpression() to Job.php if I want to show it.
|
|
||||||
// For now, let's assume we add it.
|
|
||||||
method_exists($job, 'getExpression') ? $job->getExpression() : '???',
|
|
||||||
$lastRunText,
|
|
||||||
$lastFailureText
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$table->render();
|
|
||||||
|
|
||||||
return Command::SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ use Openguru\OpenCartFramework\Config\Settings;
|
|||||||
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
class ScheduleRunCommand extends TeleCartCommand
|
class ScheduleRunCommand extends TeleCartCommand
|
||||||
@@ -24,21 +25,34 @@ class ScheduleRunCommand extends TeleCartCommand
|
|||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->addOption(
|
||||||
|
'ignore-global-lock',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Ignore global scheduler lock (e.g. when running multiple cron instances)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$mode = $this->settings->get('cron.mode', 'disabled');
|
$mode = $this->settings->get('cron.mode', 'disabled');
|
||||||
if ($mode !== 'system') {
|
if ($mode !== 'system') {
|
||||||
$output->writeln('<comment>Scheduler is disabled. Skipping CLI execution.</comment>');
|
$output->writeln('<comment>Scheduler not in CRON mode. Skipping CLI execution.</comment>');
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
$output->writeln(sprintf(
|
$output->writeln(
|
||||||
'[%s] <info>TeleCart Scheduler Running...</info>',
|
sprintf(
|
||||||
Carbon::now()->toJSON(),
|
'[%s] <info>TeleCart Scheduler Running...</info>',
|
||||||
));
|
Carbon::now()->toJSON(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
$result = $this->scheduler->run();
|
$ignoreGlobalLock = (bool) $input->getOption('ignore-global-lock');
|
||||||
|
$result = $this->scheduler->run($ignoreGlobalLock);
|
||||||
|
|
||||||
// Print Executed
|
// Print Executed
|
||||||
if (empty($result->executed)) {
|
if (empty($result->executed)) {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$sql = <<<SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS `telecart_scheduled_jobs` (
|
||||||
|
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`task` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`is_enabled` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
`cron_expression` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`last_success_at` DATETIME DEFAULT NULL,
|
||||||
|
`last_duration_seconds` FLOAT UNSIGNED NOT NULL DEFAULT 0.0,
|
||||||
|
`failed_at` DATETIME DEFAULT NULL,
|
||||||
|
`failed_reason` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL
|
||||||
|
)
|
||||||
|
ENGINE=InnoDB
|
||||||
|
DEFAULT CHARSET=utf8mb4
|
||||||
|
COLLATE=utf8mb4_unicode_ci
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
$this->database->statement($sql);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Bastion\ScheduledTasks\TeleCartPulseSendEventsTask;
|
||||||
|
use Openguru\OpenCartFramework\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->database->insert('telecart_scheduled_jobs', [
|
||||||
|
'name' => 'telecart_pulse_send_events',
|
||||||
|
'task' => TeleCartPulseSendEventsTask::class,
|
||||||
|
'is_enabled' => 0,
|
||||||
|
'cron_expression' => '*/10 * * * *',
|
||||||
|
'last_success_at' => null,
|
||||||
|
'last_duration_seconds' => 0,
|
||||||
|
'failed_at' => null,
|
||||||
|
'failed_reason' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Bastion\Services\CronApiKeyRegenerator;
|
||||||
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
|
use Openguru\OpenCartFramework\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$settings = $this->app->get(Settings::class);
|
||||||
|
$currentKey = $settings->get('cron.api_key', '');
|
||||||
|
|
||||||
|
if ($currentKey !== '') {
|
||||||
|
$this->logger->info('cron.api_key already set, migration skipped');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$regenerator = $this->app->get(CronApiKeyRegenerator::class);
|
||||||
|
$regenerator->regenerate();
|
||||||
|
$this->logger->info('cron.api_key initialized');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksService;
|
|||||||
use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksServiceProvider;
|
use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Migrations\MigrationsServiceProvider;
|
use Openguru\OpenCartFramework\Migrations\MigrationsServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Router\Router;
|
use Openguru\OpenCartFramework\Router\Router;
|
||||||
use Openguru\OpenCartFramework\Support\ExecutionTimeProfiler;
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Psr\Log\LoggerAwareInterface;
|
use Psr\Log\LoggerAwareInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -27,11 +27,6 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
|
|
||||||
private LoggerInterface $logger;
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ExecutionTimeProfiler
|
|
||||||
*/
|
|
||||||
private ExecutionTimeProfiler $profiler;
|
|
||||||
|
|
||||||
public static function getInstance(): Application
|
public static function getInstance(): Application
|
||||||
{
|
{
|
||||||
return static::$instance;
|
return static::$instance;
|
||||||
@@ -39,13 +34,6 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
|
|
||||||
private function bootKernelServices(): void
|
private function bootKernelServices(): void
|
||||||
{
|
{
|
||||||
$this->singleton(ExecutionTimeProfiler::class, function () {
|
|
||||||
return new ExecutionTimeProfiler();
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->profiler = $this->get(ExecutionTimeProfiler::class);
|
|
||||||
$this->profiler->start();
|
|
||||||
|
|
||||||
$this->singleton(Container::class, function (Container $container) {
|
$this->singleton(Container::class, function (Container $container) {
|
||||||
return $container;
|
return $container;
|
||||||
});
|
});
|
||||||
@@ -56,8 +44,6 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
return new Settings($container->getConfigValue());
|
return new Settings($container->getConfigValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->loadEnvironmentVariables();
|
|
||||||
|
|
||||||
$errorHandler = new ErrorHandler(
|
$errorHandler = new ErrorHandler(
|
||||||
$this->get(LoggerInterface::class),
|
$this->get(LoggerInterface::class),
|
||||||
$this,
|
$this,
|
||||||
@@ -68,16 +54,26 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
|
|
||||||
public function boot(): Application
|
public function boot(): Application
|
||||||
{
|
{
|
||||||
$this->bootKernelServices();
|
$timezone = $this->getConfigValue('app.oc_timezone', 'UTC');
|
||||||
|
date_default_timezone_set($timezone);
|
||||||
|
|
||||||
$this->initializeEventDispatcher(static::$events);
|
$this->loadEnvironmentVariables();
|
||||||
|
|
||||||
DependencyRegistration::register($this, $this->serviceProviders);
|
$action = $_GET['api_action'] ?? null;
|
||||||
|
|
||||||
|
SentryService::init($action);
|
||||||
|
|
||||||
|
SentryService::measure('boot_kernel_services', 'app.boot', function () {
|
||||||
|
$this->bootKernelServices();
|
||||||
|
});
|
||||||
|
|
||||||
|
SentryService::measure('dependency_registration', 'app.boot', function () {
|
||||||
|
$this->initializeEventDispatcher(static::$events);
|
||||||
|
DependencyRegistration::register($this, $this->serviceProviders);
|
||||||
|
});
|
||||||
|
|
||||||
static::$instance = $this;
|
static::$instance = $this;
|
||||||
|
|
||||||
$this->profiler->addCheckpoint('Bootstrap Application');
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,18 +93,25 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
throw new InvalidArgumentException('Invalid action: ' . $controller . '->' . $method);
|
throw new InvalidArgumentException('Invalid action: ' . $controller . '->' . $method);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->profiler->addCheckpoint('Handle Middlewares.');
|
$span = SentryService::startSpan('handle_middlewares', 'http.handle');
|
||||||
|
|
||||||
$next = fn($req) => $this->call($controller, $method);
|
try {
|
||||||
|
$next = fn($req) => $this->call($controller, $method);
|
||||||
foreach (array_reverse($this->middlewareStack) as $class) {
|
foreach (array_reverse($this->middlewareStack) as $class) {
|
||||||
$instance = $this->get($class);
|
$instance = $this->get($class);
|
||||||
$next = static fn($req) => $instance->handle($req, $next);
|
$next = static fn($req) => $instance->handle($req, $next);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = $next($request);
|
$span = SentryService::startSpan('handle_controller', 'http.handle');
|
||||||
|
|
||||||
$this->profiler->addCheckpoint('Handle HTTP request.');
|
try {
|
||||||
|
$response = $next($request);
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@@ -117,9 +120,11 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
{
|
{
|
||||||
$this->boot();
|
$this->boot();
|
||||||
|
|
||||||
$request = Request::createFromGlobals();
|
SentryService::measure('handle_request', 'http.handle', function () {
|
||||||
$response = $this->handleRequest($request);
|
$request = Request::createFromGlobals();
|
||||||
$response->send();
|
$response = $this->handleRequest($request);
|
||||||
|
$response->send();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withServiceProviders(array $serviceProviders): Application
|
public function withServiceProviders(array $serviceProviders): Application
|
||||||
@@ -179,9 +184,10 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
*/
|
*/
|
||||||
private function loadEnvironmentVariables(): void
|
private function loadEnvironmentVariables(): void
|
||||||
{
|
{
|
||||||
if (!defined('BP_PHAR_BASE_PATH') || !defined('BP_REAL_BASE_PATH')) {
|
if (! defined('BP_PHAR_BASE_PATH') || ! defined('BP_REAL_BASE_PATH')) {
|
||||||
$dotenv = Dotenv::createMutable(__DIR__ . '/../');
|
$dotenv = Dotenv::createMutable(__DIR__ . '/../');
|
||||||
$dotenv->load();
|
$dotenv->load();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Openguru\OpenCartFramework\Exceptions\ActionNotFoundException;
|
|||||||
use Openguru\OpenCartFramework\Exceptions\HttpNotFoundException;
|
use Openguru\OpenCartFramework\Exceptions\HttpNotFoundException;
|
||||||
use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException;
|
use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException;
|
||||||
use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface;
|
use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface;
|
||||||
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -110,6 +111,8 @@ class ErrorHandler
|
|||||||
|
|
||||||
public function handleShutdown(): void
|
public function handleShutdown(): void
|
||||||
{
|
{
|
||||||
|
SentryService::finishTransaction();
|
||||||
|
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
|
|
||||||
if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace Openguru\OpenCartFramework\QueryBuilder\Connections;
|
namespace Openguru\OpenCartFramework\QueryBuilder\Connections;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Openguru\OpenCartFramework\Support\Utils;
|
use Openguru\OpenCartFramework\Support\Utils;
|
||||||
use PDO;
|
use PDO;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
use Sentry\Tracing\SpanContext;
|
||||||
|
|
||||||
class MySqlConnection implements ConnectionInterface
|
class MySqlConnection implements ConnectionInterface
|
||||||
{
|
{
|
||||||
@@ -25,10 +27,23 @@ class MySqlConnection implements ConnectionInterface
|
|||||||
|
|
||||||
public function select(string $sql, array $bindings = []): array
|
public function select(string $sql, array $bindings = []): array
|
||||||
{
|
{
|
||||||
$statement = $this->pdo->prepare($sql);
|
$span = SentryService::startSpan($sql, 'db.mysql.query', function (SpanContext $context) use ($sql) {
|
||||||
$statement->execute($bindings);
|
$context->setData([
|
||||||
|
'db.system' => 'mysql',
|
||||||
|
'db.statement' => $sql,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
return $statement->fetchAll(PDO::FETCH_ASSOC);
|
try {
|
||||||
|
$statement = $this->pdo->prepare($sql);
|
||||||
|
$statement->execute($bindings);
|
||||||
|
|
||||||
|
$results = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function escape($value): string
|
public function escape($value): string
|
||||||
@@ -88,18 +103,31 @@ class MySqlConnection implements ConnectionInterface
|
|||||||
|
|
||||||
public function statement(string $sql, array $bindings = []): bool
|
public function statement(string $sql, array $bindings = []): bool
|
||||||
{
|
{
|
||||||
$statement = $this->pdo->prepare($sql);
|
$span = SentryService::startSpan($sql, 'db.mysql.query', function (SpanContext $context) use ($sql) {
|
||||||
|
$context->setData([
|
||||||
|
'db.system' => 'mysql',
|
||||||
|
'db.statement' => $sql,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
if (! $statement) {
|
try {
|
||||||
$this->lastError = $this->pdo->errorInfo();
|
$statement = $this->pdo->prepare($sql);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$success = $statement->execute($bindings);
|
if (! $statement) {
|
||||||
|
$this->lastError = $this->pdo->errorInfo();
|
||||||
|
|
||||||
if (! $success) {
|
return false;
|
||||||
$this->lastError = $statement->errorInfo();
|
}
|
||||||
return false;
|
|
||||||
|
$success = $statement->execute($bindings);
|
||||||
|
|
||||||
|
if (! $success) {
|
||||||
|
$this->lastError = $statement->errorInfo();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $success;
|
return $success;
|
||||||
@@ -147,7 +175,7 @@ class MySqlConnection implements ConnectionInterface
|
|||||||
public function insert(string $table, array $data): bool
|
public function insert(string $table, array $data): bool
|
||||||
{
|
{
|
||||||
$placeholders = implode(',', array_fill(0, count($data), '?'));
|
$placeholders = implode(',', array_fill(0, count($data), '?'));
|
||||||
$columns = implode(',', array_map(static fn ($key) => "`${key}`", array_keys($data)));
|
$columns = implode(',', array_map(static fn($key) => "`${key}`", array_keys($data)));
|
||||||
$sql = sprintf('INSERT INTO `%s` (%s) VALUES (%s)', $table, $columns, $placeholders);
|
$sql = sprintf('INSERT INTO `%s` (%s) VALUES (%s)', $table, $columns, $placeholders);
|
||||||
|
|
||||||
return $this->statement($sql, array_values($data));
|
return $this->statement($sql, array_values($data));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Openguru\OpenCartFramework\Scheduler;
|
namespace Openguru\OpenCartFramework\Scheduler;
|
||||||
|
|
||||||
use Cron\CronExpression;
|
use Cron\CronExpression;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Openguru\OpenCartFramework\Container\Container;
|
use Openguru\OpenCartFramework\Container\Container;
|
||||||
|
|
||||||
class Job
|
class Job
|
||||||
@@ -11,14 +12,15 @@ class Job
|
|||||||
|
|
||||||
/** @var string|callable|TaskInterface */
|
/** @var string|callable|TaskInterface */
|
||||||
private $action;
|
private $action;
|
||||||
|
private int $id;
|
||||||
|
private ?string $name;
|
||||||
|
|
||||||
private string $expression = '* * * * *';
|
private string $expression = '* * * * *';
|
||||||
|
|
||||||
private ?string $name;
|
public function __construct(Container $container, int $id, $action, ?string $name = null)
|
||||||
|
|
||||||
public function __construct(Container $container, $action, ?string $name = null)
|
|
||||||
{
|
{
|
||||||
$this->container = $container;
|
$this->container = $container;
|
||||||
|
$this->id = $id;
|
||||||
$this->action = $action;
|
$this->action = $action;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
}
|
}
|
||||||
@@ -74,6 +76,16 @@ class Job
|
|||||||
call_user_func($this->action);
|
call_user_func($this->action);
|
||||||
} elseif ($this->action instanceof TaskInterface) {
|
} elseif ($this->action instanceof TaskInterface) {
|
||||||
$this->action->execute();
|
$this->action->execute();
|
||||||
|
} else {
|
||||||
|
$actionType = is_object($this->action) ? get_class($this->action) : gettype($this->action);
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
sprintf(
|
||||||
|
'Job "%s" (id: %d): action is not valid (expected class name, callable or TaskInterface, got %s).',
|
||||||
|
$this->getName(),
|
||||||
|
$this->id,
|
||||||
|
$actionType
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +102,9 @@ class Job
|
|||||||
return 'Closure';
|
return 'Closure';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): string
|
public function getId(): int
|
||||||
{
|
{
|
||||||
return md5($this->getName());
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getExpression(): string
|
public function getExpression(): string
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Scheduler\Models;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Carbon\CarbonTimeZone;
|
||||||
|
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||||
|
|
||||||
|
class ScheduledJob
|
||||||
|
{
|
||||||
|
private const TABLE_NAME = 'telecart_scheduled_jobs';
|
||||||
|
|
||||||
|
private Builder $builder;
|
||||||
|
|
||||||
|
public function __construct(Builder $builder)
|
||||||
|
{
|
||||||
|
$this->builder = $builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
return $this->builder->newQuery()
|
||||||
|
->from(self::TABLE_NAME)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnabledScheduledJobs(): array
|
||||||
|
{
|
||||||
|
return $this->builder->newQuery()
|
||||||
|
->from(self::TABLE_NAME)
|
||||||
|
->where('is_enabled', '=', 1)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enable(int $id): bool
|
||||||
|
{
|
||||||
|
return $this->builder->newQuery()
|
||||||
|
->where('id', '=', $id)
|
||||||
|
->update(self::TABLE_NAME, [
|
||||||
|
'is_enabled' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable(int $id): bool
|
||||||
|
{
|
||||||
|
return $this->builder->newQuery()
|
||||||
|
->where('id', '=', $id)
|
||||||
|
->update(self::TABLE_NAME, [
|
||||||
|
'is_enabled' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCronExpression(int $id, string $cronExpression): bool
|
||||||
|
{
|
||||||
|
return $this->builder->newQuery()
|
||||||
|
->where('id', '=', $id)
|
||||||
|
->update(self::TABLE_NAME, [
|
||||||
|
'cron_expression' => mb_substr($cronExpression, 0, 64),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateLastSuccessAt(int $id, float $durationSeconds): void
|
||||||
|
{
|
||||||
|
$this->builder->newQuery()
|
||||||
|
->where('id', '=', $id)
|
||||||
|
->update(self::TABLE_NAME, [
|
||||||
|
'last_success_at' => Carbon::now(),
|
||||||
|
'last_duration_seconds' => $durationSeconds,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateFailedAt(int $id, string $message): void
|
||||||
|
{
|
||||||
|
$this->builder->newQuery()
|
||||||
|
->where('id', '=', $id)
|
||||||
|
->update(self::TABLE_NAME, [
|
||||||
|
'failed_at' => Carbon::now(),
|
||||||
|
'failed_reason' => mb_substr($message, 0, 1000),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearFailedInfo(int $id): void
|
||||||
|
{
|
||||||
|
$this->builder->newQuery()
|
||||||
|
->where('id', '=', $id)
|
||||||
|
->update(self::TABLE_NAME, [
|
||||||
|
'failed_at' => null,
|
||||||
|
'failed_reason' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Openguru\OpenCartFramework\Scheduler;
|
namespace Openguru\OpenCartFramework\Scheduler;
|
||||||
|
|
||||||
use Openguru\OpenCartFramework\Container\Container;
|
use Openguru\OpenCartFramework\Container\Container;
|
||||||
|
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
|
||||||
|
|
||||||
class ScheduleJobRegistry
|
class ScheduleJobRegistry
|
||||||
{
|
{
|
||||||
@@ -10,10 +11,12 @@ class ScheduleJobRegistry
|
|||||||
|
|
||||||
/** @var Job[] */
|
/** @var Job[] */
|
||||||
private array $jobs = [];
|
private array $jobs = [];
|
||||||
|
private ScheduledJob $scheduledJob;
|
||||||
|
|
||||||
public function __construct(Container $container)
|
public function __construct(Container $container, ScheduledJob $scheduledJob)
|
||||||
{
|
{
|
||||||
$this->container = $container;
|
$this->container = $container;
|
||||||
|
$this->scheduledJob = $scheduledJob;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,9 +24,9 @@ class ScheduleJobRegistry
|
|||||||
* @param string|null $name
|
* @param string|null $name
|
||||||
* @return Job
|
* @return Job
|
||||||
*/
|
*/
|
||||||
public function add($job, ?string $name = null): Job
|
public function add(int $id, $job, ?string $name = null): Job
|
||||||
{
|
{
|
||||||
$newJob = new Job($this->container, $job, $name);
|
$newJob = new Job($this->container, $id, $job, $name);
|
||||||
$this->jobs[] = $newJob;
|
$this->jobs[] = $newJob;
|
||||||
|
|
||||||
return $newJob;
|
return $newJob;
|
||||||
@@ -36,4 +39,14 @@ class ScheduleJobRegistry
|
|||||||
{
|
{
|
||||||
return $this->jobs;
|
return $this->jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function loadJobsFromDatabase(): void
|
||||||
|
{
|
||||||
|
$jobs = $this->scheduledJob->getEnabledScheduledJobs();
|
||||||
|
|
||||||
|
foreach ($jobs as $job) {
|
||||||
|
$this->add($job['id'], $job['task'], $job['name'])
|
||||||
|
->at($job['cron_expression']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Openguru\OpenCartFramework\Scheduler;
|
namespace Openguru\OpenCartFramework\Scheduler;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||||
use Openguru\OpenCartFramework\Config\Settings;
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
use Openguru\OpenCartFramework\Container\Container;
|
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -13,23 +15,25 @@ class SchedulerService
|
|||||||
{
|
{
|
||||||
private LoggerInterface $logger;
|
private LoggerInterface $logger;
|
||||||
private CacheInterface $cache;
|
private CacheInterface $cache;
|
||||||
private Container $container;
|
|
||||||
private Settings $settings;
|
private Settings $settings;
|
||||||
private ?ScheduleJobRegistry $registry = null;
|
private ScheduleJobRegistry $registry;
|
||||||
|
|
||||||
private const GLOBAL_LOCK_KEY = 'scheduler.global_lock';
|
private const GLOBAL_LOCK_KEY = 'scheduler.global_lock';
|
||||||
private const GLOBAL_LOCK_TTL = 300; // 5 minutes
|
private const GLOBAL_LOCK_TTL = 300; // 5 minutes
|
||||||
|
private ScheduledJob $scheduledJob;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
CacheInterface $cache,
|
CacheInterface $cache,
|
||||||
Container $container,
|
Settings $settings,
|
||||||
Settings $settings
|
ScheduleJobRegistry $registry,
|
||||||
|
ScheduledJob $scheduledJob
|
||||||
) {
|
) {
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
$this->container = $container;
|
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
|
$this->registry = $registry;
|
||||||
|
$this->scheduledJob = $scheduledJob;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For testing purposes
|
// For testing purposes
|
||||||
@@ -38,7 +42,7 @@ class SchedulerService
|
|||||||
$this->registry = $registry;
|
$this->registry = $registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): SchedulerResult
|
public function run(bool $ignoreGlobalLock = false): SchedulerResult
|
||||||
{
|
{
|
||||||
$result = new SchedulerResult();
|
$result = new SchedulerResult();
|
||||||
|
|
||||||
@@ -49,40 +53,30 @@ class SchedulerService
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->isGlobalLocked()) {
|
if (! $ignoreGlobalLock && $this->isGlobalLocked()) {
|
||||||
$result->addSkipped('Global', 'Global scheduler lock active');
|
$result->addSkipped('Global', 'Global scheduler lock active');
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->acquireGlobalLock();
|
if (! $ignoreGlobalLock) {
|
||||||
// Since we want to run every 5 minutes, running it more frequently won't trigger jobs earlier than due,
|
$this->acquireGlobalLock();
|
||||||
// but locking might prevent overlap if previous run takes > 5 mins.
|
}
|
||||||
// However, updating global last run on every attempt might be useful for diagnostics,
|
|
||||||
// but strictly speaking, we only care if tasks were processed.
|
|
||||||
$this->updateGlobalLastRun();
|
$this->updateGlobalLastRun();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$scheduler = $this->registry ?: new ScheduleJobRegistry($this->container);
|
$this->registry->loadJobsFromDatabase();
|
||||||
|
|
||||||
// Only load config file if registry was not injected (for production use)
|
foreach ($this->registry->getJobs() as $job) {
|
||||||
if (! $this->registry) {
|
|
||||||
$configFile = BP_PHAR_BASE_PATH . '/configs/schedule.php';
|
|
||||||
if (file_exists($configFile)) {
|
|
||||||
require $configFile;
|
|
||||||
} else {
|
|
||||||
$this->logger->warning('Scheduler config file not found: ' . $configFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($scheduler->getJobs() as $job) {
|
|
||||||
$this->processJob($job, $result);
|
$this->processJob($job, $result);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->logger->error('Scheduler run failed: ' . $e->getMessage(), ['exception' => $e]);
|
$this->logger->error('Scheduler run failed: ' . $e->getMessage(), ['exception' => $e]);
|
||||||
$result->addFailed('Scheduler', $e->getMessage());
|
$result->addFailed('Scheduler', $e->getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
$this->releaseGlobalLock();
|
if (! $ignoreGlobalLock) {
|
||||||
|
$this->releaseGlobalLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
@@ -93,50 +87,44 @@ class SchedulerService
|
|||||||
$name = $job->getName();
|
$name = $job->getName();
|
||||||
$id = $job->getId();
|
$id = $job->getId();
|
||||||
|
|
||||||
|
// 1. Check if due by Cron expression
|
||||||
|
if (! $job->isDue()) {
|
||||||
|
$result->addSkipped($name, 'Not due');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Last Run (Prevent running multiple times in the same minute)
|
||||||
|
if ($this->hasRanRecently($id)) {
|
||||||
|
$result->addSkipped($name, 'Already ran recently');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Lock (Prevent parallel execution)
|
||||||
|
if ($this->isJobLocked($id)) {
|
||||||
|
$result->addSkipped($name, 'Job is locked (running)');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lockJob($id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Check if due by Cron expression
|
$this->scheduledJob->clearFailedInfo($id);
|
||||||
if (! $job->isDue()) {
|
|
||||||
$result->addSkipped($name, 'Not due');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check Last Run (Prevent running multiple times in the same minute)
|
|
||||||
if ($this->hasRanRecently($id)) {
|
|
||||||
$result->addSkipped($name, 'Already ran recently');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check Lock (Prevent parallel execution)
|
|
||||||
if ($this->isJobLocked($id)) {
|
|
||||||
$result->addSkipped($name, 'Job is locked (running)');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock and Run
|
|
||||||
$this->lockJob($id);
|
|
||||||
$startTime = microtime(true);
|
$startTime = microtime(true);
|
||||||
|
$job->run();
|
||||||
try {
|
$duration = microtime(true) - $startTime;
|
||||||
$job->run();
|
$this->scheduledJob->updateLastSuccessAt($id, $duration);
|
||||||
|
$this->logger->debug("Job executed: {$name}", ['duration' => $duration]);
|
||||||
$duration = microtime(true) - $startTime;
|
$result->addExecuted($name, $duration);
|
||||||
$this->updateLastRun($id);
|
|
||||||
|
|
||||||
$this->logger->debug("Job executed: {$name}", ['duration' => $duration]);
|
|
||||||
$result->addExecuted($name, $duration);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->updateLastFailure($id, $e->getMessage());
|
|
||||||
$this->logger->error("Job failed: {$name}", ['exception' => $e]);
|
|
||||||
$result->addFailed($name, $e->getMessage());
|
|
||||||
} finally {
|
|
||||||
$this->unlockJob($id);
|
|
||||||
}
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->logger->error("Error processing job {$name}: " . $e->getMessage());
|
$this->logger->error("Job failed: {$name}", ['exception' => $e]);
|
||||||
$result->addFailed($name, 'Processing error: ' . $e->getMessage());
|
$this->scheduledJob->updateFailedAt($id, $e->getMessage());
|
||||||
|
$result->addFailed($name, $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
$this->updateLastRun($id);
|
||||||
|
$this->unlockJob($id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,36 +143,36 @@ class SchedulerService
|
|||||||
$this->cache->delete(self::GLOBAL_LOCK_KEY);
|
$this->cache->delete(self::GLOBAL_LOCK_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isJobLocked(string $id): bool
|
private function isJobLocked(int $id): bool
|
||||||
{
|
{
|
||||||
return (bool) $this->cache->get("scheduler.lock.{$id}");
|
return (bool) $this->cache->get("scheduler.lock.{$id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function lockJob(string $id): void
|
private function lockJob(int $id): void
|
||||||
{
|
{
|
||||||
// 30 minutes max execution time for a single job safe-guard
|
// 30 minutes max execution time for a single job safe-guard
|
||||||
$this->cache->set("scheduler.lock.{$id}", 1, 1800);
|
$this->cache->set("scheduler.lock.{$id}", 1, 1800);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function unlockJob(string $id): void
|
private function unlockJob(int $id): void
|
||||||
{
|
{
|
||||||
$this->cache->delete("scheduler.lock.{$id}");
|
$this->cache->delete("scheduler.lock.{$id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hasRanRecently(string $id): bool
|
private function hasRanRecently(int $id): bool
|
||||||
{
|
{
|
||||||
$lastRun = $this->getLastRun($id);
|
$lastRun = $this->getLastRun($id);
|
||||||
if (! $lastRun) {
|
if (! $lastRun) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lastRunDate = (new DateTime())->setTimestamp((int) $lastRun);
|
$lastRunDate = (new DateTime())->setTimestamp($lastRun);
|
||||||
$now = new DateTime();
|
$now = new DateTime();
|
||||||
|
|
||||||
return $lastRunDate->format('Y-m-d H:i') === $now->format('Y-m-d H:i');
|
return $lastRunDate->format('Y-m-d H:i') === $now->format('Y-m-d H:i');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateLastRun(string $id): void
|
private function updateLastRun(int $id): void
|
||||||
{
|
{
|
||||||
$this->cache->set("scheduler.last_run.{$id}", time());
|
$this->cache->set("scheduler.last_run.{$id}", time());
|
||||||
}
|
}
|
||||||
@@ -194,34 +182,8 @@ class SchedulerService
|
|||||||
$this->cache->set("scheduler.global_last_run", time());
|
$this->cache->set("scheduler.global_last_run", time());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getGlobalLastRun(): ?int
|
public function getLastRun(int $id): ?int
|
||||||
{
|
|
||||||
$time = $this->cache->get("scheduler.global_last_run");
|
|
||||||
|
|
||||||
return $time ? (int) $time : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateLastFailure(string $id, string $message): void
|
|
||||||
{
|
|
||||||
$this->cache->set("scheduler.last_failure.{$id}", time());
|
|
||||||
$this->cache->set("scheduler.last_failure_msg.{$id}", $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLastRun(string $id): ?int
|
|
||||||
{
|
{
|
||||||
return $this->cache->get("scheduler.last_run.{$id}");
|
return $this->cache->get("scheduler.last_run.{$id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLastFailure(string $id): ?array
|
|
||||||
{
|
|
||||||
$time = $this->cache->get("scheduler.last_failure.{$id}");
|
|
||||||
if (! $time) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'time' => (int) $time,
|
|
||||||
'message' => $this->cache->get("scheduler.last_failure_msg.{$id}"),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,12 @@
|
|||||||
|
|
||||||
namespace Openguru\OpenCartFramework\Scheduler;
|
namespace Openguru\OpenCartFramework\Scheduler;
|
||||||
|
|
||||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
|
||||||
use Openguru\OpenCartFramework\Config\Settings;
|
|
||||||
use Openguru\OpenCartFramework\Container\Container;
|
|
||||||
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
use Openguru\OpenCartFramework\Container\ServiceProvider;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
|
|
||||||
class SchedulerServiceProvider extends ServiceProvider
|
class SchedulerServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
$this->container->singleton(SchedulerService::class, function (Container $container) {
|
//
|
||||||
return new SchedulerService(
|
|
||||||
$container->get(LoggerInterface::class),
|
|
||||||
$container->get(CacheInterface::class),
|
|
||||||
$container,
|
|
||||||
$container->get(Settings::class)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Sentry;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Sentry\SentrySdk;
|
||||||
|
use Sentry\Tracing\Span;
|
||||||
|
use Sentry\Tracing\SpanContext;
|
||||||
|
use Sentry\Tracing\Transaction;
|
||||||
|
use Sentry\Tracing\TransactionContext;
|
||||||
|
use SplObjectStorage;
|
||||||
|
|
||||||
|
use function Sentry\init;
|
||||||
|
use function Sentry\startTransaction;
|
||||||
|
|
||||||
|
class SentryService
|
||||||
|
{
|
||||||
|
private static ?string $currentAction = null;
|
||||||
|
public static ?Transaction $currentTransaction = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores parent span for each created child span to correctly restore nesting.
|
||||||
|
*
|
||||||
|
* @var SplObjectStorage<Span, Span|null>|null
|
||||||
|
*/
|
||||||
|
private static ?SplObjectStorage $spanParents = null;
|
||||||
|
|
||||||
|
public static array $excludeActions = [
|
||||||
|
'health',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static function resolveOptionsFromEnv(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'dsn' => env('SENTRY_DSN'),
|
||||||
|
'send_default_pii' => true,
|
||||||
|
'enable_logs' => (bool) filter_var(env('SENTRY_ENABLE_LOGS', false), FILTER_VALIDATE_BOOLEAN),
|
||||||
|
'traces_sample_rate' => (float) env('SENTRY_TRACES_SAMPLE_RATE', 1.0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function isSentryEnabled(): bool
|
||||||
|
{
|
||||||
|
return self::$currentAction !== null
|
||||||
|
&& ! in_array(self::$currentAction, self::$excludeActions, true)
|
||||||
|
&& env('SENTRY_ENABLED', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function init(?string $action): void
|
||||||
|
{
|
||||||
|
$options = self::resolveOptionsFromEnv();
|
||||||
|
self::$currentAction = $action;
|
||||||
|
|
||||||
|
if (! self::isSentryEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
init($options);
|
||||||
|
|
||||||
|
$transactionContext = TransactionContext::make()
|
||||||
|
->setName(self::$currentAction)
|
||||||
|
->setOp('http.server');
|
||||||
|
|
||||||
|
self::$currentTransaction = startTransaction($transactionContext);
|
||||||
|
SentrySdk::getCurrentHub()->setSpan(self::$currentTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function startSpan(string $description, string $op, ?Closure $closure = null): ?Span
|
||||||
|
{
|
||||||
|
$parent = self::resolveParent();
|
||||||
|
|
||||||
|
if (! self::isSentryEnabled() || ! $parent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = SpanContext::make()
|
||||||
|
->setOp($op)
|
||||||
|
->setDescription($description);
|
||||||
|
|
||||||
|
if ($closure) {
|
||||||
|
$closure($context);
|
||||||
|
}
|
||||||
|
|
||||||
|
$span = $parent->startChild($context);
|
||||||
|
|
||||||
|
if (self::$spanParents === null) {
|
||||||
|
self::$spanParents = new SplObjectStorage();
|
||||||
|
}
|
||||||
|
self::$spanParents[$span] = $parent;
|
||||||
|
|
||||||
|
SentrySdk::getCurrentHub()->setSpan($span);
|
||||||
|
|
||||||
|
return $span;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function endSpan(?Span $span = null): void
|
||||||
|
{
|
||||||
|
if (! $span || ! self::isSentryEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent = null;
|
||||||
|
if (self::$spanParents !== null && self::$spanParents->contains($span)) {
|
||||||
|
$parent = self::$spanParents[$span] ?? null;
|
||||||
|
self::$spanParents->detach($span);
|
||||||
|
}
|
||||||
|
|
||||||
|
$span->finish();
|
||||||
|
SentrySdk::getCurrentHub()->setSpan($parent ?? self::$currentTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveParent(): ?Span
|
||||||
|
{
|
||||||
|
return SentrySdk::getCurrentHub()->getSpan() ?? self::$currentTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function measure(string $description, string $op, Closure $closure, ?Closure $params = null): void
|
||||||
|
{
|
||||||
|
$span = self::startSpan($description, $op, $params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$closure();
|
||||||
|
} finally {
|
||||||
|
self::endSpan($span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function finishTransaction(): void
|
||||||
|
{
|
||||||
|
if (self::$currentTransaction) {
|
||||||
|
self::$currentTransaction->finish();
|
||||||
|
self::$currentTransaction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ class TelegramValidateInitDataMiddleware
|
|||||||
'health',
|
'health',
|
||||||
'etlCustomers',
|
'etlCustomers',
|
||||||
'etlCustomersMeta',
|
'etlCustomersMeta',
|
||||||
|
'runSchedule',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(SignatureValidator $signatureValidator)
|
public function __construct(SignatureValidator $signatureValidator)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use Openguru\OpenCartFramework\QueryBuilder\QueryBuilderServiceProvider;
|
|||||||
use Openguru\OpenCartFramework\Router\RouteServiceProvider;
|
use Openguru\OpenCartFramework\Router\RouteServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseServiceProvider;
|
use Openguru\OpenCartFramework\TeleCartPulse\TeleCartPulseServiceProvider;
|
||||||
|
use Openguru\OpenCartFramework\Scheduler\SchedulerServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider;
|
use Openguru\OpenCartFramework\Telegram\TelegramServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Telegram\TelegramValidateInitDataMiddleware;
|
use Openguru\OpenCartFramework\Telegram\TelegramValidateInitDataMiddleware;
|
||||||
use Openguru\OpenCartFramework\Validator\ValidatorServiceProvider;
|
use Openguru\OpenCartFramework\Validator\ValidatorServiceProvider;
|
||||||
@@ -31,6 +32,7 @@ class ApplicationFactory
|
|||||||
RouteServiceProvider::class,
|
RouteServiceProvider::class,
|
||||||
AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
TelegramServiceProvider::class,
|
TelegramServiceProvider::class,
|
||||||
|
SchedulerServiceProvider::class,
|
||||||
ValidatorServiceProvider::class,
|
ValidatorServiceProvider::class,
|
||||||
TeleCartPulseServiceProvider::class,
|
TeleCartPulseServiceProvider::class,
|
||||||
ImageToolServiceProvider::class,
|
ImageToolServiceProvider::class,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Handlers;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
|
use Openguru\OpenCartFramework\Http\Request;
|
||||||
|
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class CronHandler
|
||||||
|
{
|
||||||
|
private Settings $settings;
|
||||||
|
private SchedulerService $schedulerService;
|
||||||
|
|
||||||
|
public function __construct(Settings $settings, SchedulerService $schedulerService)
|
||||||
|
{
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->schedulerService = $schedulerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск планировщика по HTTP (для cron-job.org и аналогов).
|
||||||
|
* Требует api_key в query, совпадающий с настройкой cron.api_key.
|
||||||
|
*/
|
||||||
|
public function runSchedule(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$mode = $this->settings->get('cron.mode', 'disabled');
|
||||||
|
if ($mode !== 'cron_job_org') {
|
||||||
|
return new JsonResponse(['error' => 'Scheduler is not in cron-job.org mode'], Response::HTTP_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $request->get('api_key', '');
|
||||||
|
$expectedKey = $this->settings->get('cron.api_key', '');
|
||||||
|
if ($expectedKey === '' || $apiKey === '' || !hash_equals($expectedKey, $apiKey)) {
|
||||||
|
return new JsonResponse(['error' => 'Invalid or missing API key'], Response::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Увеличиваем лимит времени выполнения при запуске по HTTP, чтобы снизить риск timeout
|
||||||
|
$limit = 300; // 5 минут
|
||||||
|
if (function_exists('set_time_limit')) {
|
||||||
|
@set_time_limit($limit);
|
||||||
|
}
|
||||||
|
if (function_exists('ini_set')) {
|
||||||
|
@ini_set('max_execution_time', (string) $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->schedulerService->run(true);
|
||||||
|
$data = [
|
||||||
|
'success' => true,
|
||||||
|
'executed' => count($result->executed),
|
||||||
|
'failed' => count($result->failed),
|
||||||
|
'skipped' => count($result->skipped),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new JsonResponse($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
|||||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Table;
|
use Openguru\OpenCartFramework\QueryBuilder\Table;
|
||||||
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
use Openguru\OpenCartFramework\Support\PaginationHelper;
|
use Openguru\OpenCartFramework\Support\PaginationHelper;
|
||||||
use Openguru\OpenCartFramework\Support\Str;
|
use Openguru\OpenCartFramework\Support\Str;
|
||||||
@@ -64,7 +65,7 @@ class ProductsService
|
|||||||
$perPage = $params['perPage'];
|
$perPage = $params['perPage'];
|
||||||
$search = $params['search'] ?? false;
|
$search = $params['search'] ?? false;
|
||||||
$categoryName = '';
|
$categoryName = '';
|
||||||
$maxPages = $params['maxPages'] ?? 50;
|
$maxPages = 200;
|
||||||
$filters = $params['filters'] ?? [];
|
$filters = $params['filters'] ?? [];
|
||||||
|
|
||||||
$aspectRatio = $this->settings->get('app.image_aspect_ratio', '1:1');
|
$aspectRatio = $this->settings->get('app.image_aspect_ratio', '1:1');
|
||||||
@@ -157,6 +158,7 @@ class ProductsService
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$span = SentryService::startSpan('crop_images', 'image.process');
|
||||||
$productsImagesMap = [];
|
$productsImagesMap = [];
|
||||||
foreach ($productsImages as $item) {
|
foreach ($productsImages as $item) {
|
||||||
$productId = $item['product_id'];
|
$productId = $item['product_id'];
|
||||||
@@ -174,6 +176,8 @@ class ProductsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
|
||||||
$debug = [];
|
$debug = [];
|
||||||
if (env('APP_DEBUG')) {
|
if (env('APP_DEBUG')) {
|
||||||
$debug = [
|
$debug = [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use App\Handlers\BlocksHandler;
|
use App\Handlers\BlocksHandler;
|
||||||
use App\Handlers\CartHandler;
|
use App\Handlers\CartHandler;
|
||||||
use App\Handlers\CategoriesHandler;
|
use App\Handlers\CategoriesHandler;
|
||||||
|
use App\Handlers\CronHandler;
|
||||||
use App\Handlers\ETLHandler;
|
use App\Handlers\ETLHandler;
|
||||||
use App\Handlers\FiltersHandler;
|
use App\Handlers\FiltersHandler;
|
||||||
use App\Handlers\FormsHandler;
|
use App\Handlers\FormsHandler;
|
||||||
@@ -24,6 +25,7 @@ return [
|
|||||||
'getForm' => [FormsHandler::class, 'getForm'],
|
'getForm' => [FormsHandler::class, 'getForm'],
|
||||||
'health' => [HealthCheckHandler::class, 'handle'],
|
'health' => [HealthCheckHandler::class, 'handle'],
|
||||||
'ingest' => [TelemetryHandler::class, 'ingest'],
|
'ingest' => [TelemetryHandler::class, 'ingest'],
|
||||||
|
'runSchedule' => [CronHandler::class, 'runSchedule'],
|
||||||
'heartbeat' => [TelemetryHandler::class, 'heartbeat'],
|
'heartbeat' => [TelemetryHandler::class, 'heartbeat'],
|
||||||
'processBlock' => [BlocksHandler::class, 'processBlock'],
|
'processBlock' => [BlocksHandler::class, 'processBlock'],
|
||||||
'product_show' => [ProductsHandler::class, 'show'],
|
'product_show' => [ProductsHandler::class, 'show'],
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ class JobTest extends TestCase
|
|||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job = new Job($container, function() {}, 'TestJob');
|
$job = new Job($container, 1, function() {}, 'TestJob');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertEquals('TestJob', $job->getName());
|
$this->assertEquals('TestJob', $job->getName());
|
||||||
$this->assertEquals(md5('TestJob'), $job->getId());
|
$this->assertEquals(1, $job->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testJobWithoutNameUsesClassName()
|
public function testJobWithoutNameUsesClassName()
|
||||||
@@ -27,7 +27,7 @@ class JobTest extends TestCase
|
|||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job = new Job($container, TestTask::class);
|
$job = new Job($container, 1, TestTask::class);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertEquals(TestTask::class, $job->getName());
|
$this->assertEquals(TestTask::class, $job->getName());
|
||||||
@@ -39,7 +39,7 @@ class JobTest extends TestCase
|
|||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertEquals('Closure', $job->getName());
|
$this->assertEquals('Closure', $job->getName());
|
||||||
@@ -49,7 +49,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->everyMinute();
|
$job->everyMinute();
|
||||||
@@ -62,7 +62,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->everyFiveMinutes();
|
$job->everyFiveMinutes();
|
||||||
@@ -75,7 +75,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->everyHour();
|
$job->everyHour();
|
||||||
@@ -88,7 +88,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->dailyAt(9, 30);
|
$job->dailyAt(9, 30);
|
||||||
@@ -101,7 +101,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->at('0 12 * * 1');
|
$job->at('0 12 * * 1');
|
||||||
@@ -114,7 +114,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
$job->everyMinute();
|
$job->everyMinute();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -128,7 +128,7 @@ class JobTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$job = new Job($container, function() {});
|
$job = new Job($container, 1, function() {});
|
||||||
// Set to run at a specific future time
|
// Set to run at a specific future time
|
||||||
$job->at('0 23 * * *'); // 23:00 every day
|
$job->at('0 23 * * *'); // 23:00 every day
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class JobTest extends TestCase
|
|||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$executed = false;
|
$executed = false;
|
||||||
|
|
||||||
$job = new Job($container, function() use (&$executed) {
|
$job = new Job($container, 1, function() use (&$executed) {
|
||||||
$executed = true;
|
$executed = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ class JobTest extends TestCase
|
|||||||
return new TestTask();
|
return new TestTask();
|
||||||
});
|
});
|
||||||
|
|
||||||
$job = new Job($container, TestTask::class);
|
$job = new Job($container, 1, TestTask::class);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->run();
|
$job->run();
|
||||||
@@ -182,7 +182,7 @@ class JobTest extends TestCase
|
|||||||
// Arrange
|
// Arrange
|
||||||
$container = $this->app;
|
$container = $this->app;
|
||||||
$task = new TestTask();
|
$task = new TestTask();
|
||||||
$job = new Job($container, $task);
|
$job = new Job($container, 1, $task);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job->run();
|
$job->run();
|
||||||
|
|||||||
@@ -2,16 +2,23 @@
|
|||||||
|
|
||||||
namespace Tests\Unit\Framework\Scheduler;
|
namespace Tests\Unit\Framework\Scheduler;
|
||||||
|
|
||||||
|
use Mockery;
|
||||||
use Openguru\OpenCartFramework\Scheduler\Job;
|
use Openguru\OpenCartFramework\Scheduler\Job;
|
||||||
|
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
|
||||||
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
|
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class ScheduleJobRegistryTest extends TestCase
|
class ScheduleJobRegistryTest extends TestCase
|
||||||
{
|
{
|
||||||
|
private function createScheduledJobMock(): ScheduledJob
|
||||||
|
{
|
||||||
|
return Mockery::mock(ScheduledJob::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function testRegistryCreation()
|
public function testRegistryCreation()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
$registry = new ScheduleJobRegistry($this->app);
|
$registry = new ScheduleJobRegistry($this->app, $this->createScheduledJobMock());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertInstanceOf(ScheduleJobRegistry::class, $registry);
|
$this->assertInstanceOf(ScheduleJobRegistry::class, $registry);
|
||||||
@@ -21,10 +28,10 @@ class ScheduleJobRegistryTest extends TestCase
|
|||||||
public function testAddJobWithoutName()
|
public function testAddJobWithoutName()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$registry = new ScheduleJobRegistry($this->app);
|
$registry = new ScheduleJobRegistry($this->app, $this->createScheduledJobMock());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job = $registry->add(function() {});
|
$job = $registry->add(1, function() {});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertInstanceOf(Job::class, $job);
|
$this->assertInstanceOf(Job::class, $job);
|
||||||
@@ -35,10 +42,10 @@ class ScheduleJobRegistryTest extends TestCase
|
|||||||
public function testAddJobWithName()
|
public function testAddJobWithName()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$registry = new ScheduleJobRegistry($this->app);
|
$registry = new ScheduleJobRegistry($this->app, $this->createScheduledJobMock());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job = $registry->add(function() {}, 'MyCustomJob');
|
$job = $registry->add(1, function() {}, 'MyCustomJob');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertInstanceOf(Job::class, $job);
|
$this->assertInstanceOf(Job::class, $job);
|
||||||
@@ -49,12 +56,12 @@ class ScheduleJobRegistryTest extends TestCase
|
|||||||
public function testAddMultipleJobs()
|
public function testAddMultipleJobs()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$registry = new ScheduleJobRegistry($this->app);
|
$registry = new ScheduleJobRegistry($this->app, $this->createScheduledJobMock());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job1 = $registry->add(function() {}, 'Job1');
|
$job1 = $registry->add(1, function() {}, 'Job1');
|
||||||
$job2 = $registry->add(function() {}, 'Job2');
|
$job2 = $registry->add(2, function() {}, 'Job2');
|
||||||
$job3 = $registry->add(TestTask::class, 'Job3');
|
$job3 = $registry->add(3, TestTask::class, 'Job3');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$jobs = $registry->getJobs();
|
$jobs = $registry->getJobs();
|
||||||
@@ -67,8 +74,8 @@ class ScheduleJobRegistryTest extends TestCase
|
|||||||
public function testGetJobsReturnsArray()
|
public function testGetJobsReturnsArray()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$registry = new ScheduleJobRegistry($this->app);
|
$registry = new ScheduleJobRegistry($this->app, $this->createScheduledJobMock());
|
||||||
$registry->add(function() {}, 'TestJob');
|
$registry->add(1, function() {}, 'TestJob');
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$jobs = $registry->getJobs();
|
$jobs = $registry->getJobs();
|
||||||
@@ -82,10 +89,10 @@ class ScheduleJobRegistryTest extends TestCase
|
|||||||
public function testJobSchedulingMethods()
|
public function testJobSchedulingMethods()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$registry = new ScheduleJobRegistry($this->app);
|
$registry = new ScheduleJobRegistry($this->app, $this->createScheduledJobMock());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
$job = $registry->add(function() {}, 'TestJob')
|
$job = $registry->add(1, function() {}, 'TestJob')
|
||||||
->everyFiveMinutes();
|
->everyFiveMinutes();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Mockery;
|
|||||||
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
use Openguru\OpenCartFramework\Cache\CacheInterface;
|
||||||
use Openguru\OpenCartFramework\Config\Settings;
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
use Openguru\OpenCartFramework\Scheduler\Job;
|
use Openguru\OpenCartFramework\Scheduler\Job;
|
||||||
|
use Openguru\OpenCartFramework\Scheduler\Models\ScheduledJob;
|
||||||
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
use Openguru\OpenCartFramework\Scheduler\SchedulerService;
|
||||||
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
|
use Openguru\OpenCartFramework\Scheduler\ScheduleJobRegistry;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -26,11 +27,21 @@ class SchedulerServiceTest extends TestCase
|
|||||||
$this->settingsMock = Mockery::mock(Settings::class);
|
$this->settingsMock = Mockery::mock(Settings::class);
|
||||||
$this->loggerMock = Mockery::mock(LoggerInterface::class);
|
$this->loggerMock = Mockery::mock(LoggerInterface::class);
|
||||||
|
|
||||||
|
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
||||||
|
$registryMock->shouldReceive('loadJobsFromDatabase')->andReturnNull();
|
||||||
|
$registryMock->shouldReceive('getJobs')->andReturn([]);
|
||||||
|
|
||||||
|
$scheduledJobMock = Mockery::mock(ScheduledJob::class);
|
||||||
|
$scheduledJobMock->shouldReceive('clearFailedInfo')->zeroOrMoreTimes()->andReturnNull();
|
||||||
|
$scheduledJobMock->shouldReceive('updateLastSuccessAt')->zeroOrMoreTimes()->andReturnNull();
|
||||||
|
$scheduledJobMock->shouldReceive('updateFailedAt')->zeroOrMoreTimes()->andReturnNull();
|
||||||
|
|
||||||
$this->scheduler = new SchedulerService(
|
$this->scheduler = new SchedulerService(
|
||||||
$this->loggerMock,
|
$this->loggerMock,
|
||||||
$this->cacheMock,
|
$this->cacheMock,
|
||||||
$this->app,
|
$this->settingsMock,
|
||||||
$this->settingsMock
|
$registryMock,
|
||||||
|
$scheduledJobMock
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,32 +102,33 @@ class SchedulerServiceTest extends TestCase
|
|||||||
->with('scheduler.global_lock', 1, 300)
|
->with('scheduler.global_lock', 1, 300)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
|
||||||
->with('scheduler.global_lock')
|
|
||||||
->once();
|
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.global_last_run', Mockery::type('int'))
|
->with('scheduler.global_last_run', Mockery::type('int'))
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
// Mock registry and job
|
// Mock registry and job
|
||||||
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
||||||
|
$registryMock->shouldReceive('loadJobsFromDatabase')->andReturnNull();
|
||||||
$jobMock = Mockery::mock(Job::class);
|
$jobMock = Mockery::mock(Job::class);
|
||||||
|
|
||||||
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
||||||
$jobMock->shouldReceive('getId')->andReturn('test_job_id');
|
$jobMock->shouldReceive('getId')->andReturn(42);
|
||||||
$jobMock->shouldReceive('isDue')->andReturn(true);
|
$jobMock->shouldReceive('isDue')->andReturn(true);
|
||||||
|
|
||||||
// Job has not run recently (getLastRun returns null)
|
// Job has not run recently (getLastRun returns null)
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.last_run.test_job_id')
|
->with('scheduler.last_run.42')
|
||||||
->andReturn(null);
|
->andReturn(null);
|
||||||
|
|
||||||
// Job is locked
|
// Job is locked
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.lock.test_job_id')
|
->with('scheduler.lock.42')
|
||||||
->andReturn('1');
|
->andReturn('1');
|
||||||
|
|
||||||
|
$this->cacheMock->shouldReceive('delete')
|
||||||
|
->with('scheduler.global_lock')
|
||||||
|
->once();
|
||||||
|
|
||||||
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
||||||
|
|
||||||
// Logger should not be called for this test
|
// Logger should not be called for this test
|
||||||
@@ -147,32 +159,33 @@ class SchedulerServiceTest extends TestCase
|
|||||||
->with('scheduler.global_lock', 1, 300)
|
->with('scheduler.global_lock', 1, 300)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
|
||||||
->with('scheduler.global_lock')
|
|
||||||
->once();
|
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.global_last_run', Mockery::type('int'))
|
->with('scheduler.global_last_run', Mockery::type('int'))
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
// Mock registry and job
|
// Mock registry and job
|
||||||
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
||||||
|
$registryMock->shouldReceive('loadJobsFromDatabase')->andReturnNull();
|
||||||
$jobMock = Mockery::mock(Job::class);
|
$jobMock = Mockery::mock(Job::class);
|
||||||
|
|
||||||
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
||||||
$jobMock->shouldReceive('getId')->andReturn('test_job_id');
|
$jobMock->shouldReceive('getId')->andReturn(42);
|
||||||
$jobMock->shouldReceive('isDue')->andReturn(true);
|
$jobMock->shouldReceive('isDue')->andReturn(true);
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.lock.test_job_id')
|
->with('scheduler.lock.42')
|
||||||
->andReturn(null); // Not locked
|
->andReturn(null); // Not locked
|
||||||
|
|
||||||
// Job was recently executed (same minute)
|
// Job was recently executed (same minute)
|
||||||
$recentTime = time();
|
$recentTime = time();
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.last_run.test_job_id')
|
->with('scheduler.last_run.42')
|
||||||
->andReturn($recentTime);
|
->andReturn($recentTime);
|
||||||
|
|
||||||
|
$this->cacheMock->shouldReceive('delete')
|
||||||
|
->with('scheduler.global_lock')
|
||||||
|
->once();
|
||||||
|
|
||||||
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
||||||
|
|
||||||
// Inject registry for testing
|
// Inject registry for testing
|
||||||
@@ -200,22 +213,23 @@ class SchedulerServiceTest extends TestCase
|
|||||||
->with('scheduler.global_lock', 1, 300)
|
->with('scheduler.global_lock', 1, 300)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
|
||||||
->with('scheduler.global_lock')
|
|
||||||
->once();
|
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.global_last_run', Mockery::type('int'))
|
->with('scheduler.global_last_run', Mockery::type('int'))
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
// Mock registry and job
|
// Mock registry and job
|
||||||
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
||||||
|
$registryMock->shouldReceive('loadJobsFromDatabase')->andReturnNull();
|
||||||
$jobMock = Mockery::mock(Job::class);
|
$jobMock = Mockery::mock(Job::class);
|
||||||
|
|
||||||
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
||||||
$jobMock->shouldReceive('getId')->andReturn('test_job_id');
|
$jobMock->shouldReceive('getId')->andReturn(42);
|
||||||
$jobMock->shouldReceive('isDue')->andReturn(false); // Not due
|
$jobMock->shouldReceive('isDue')->andReturn(false); // Not due
|
||||||
|
|
||||||
|
$this->cacheMock->shouldReceive('delete')
|
||||||
|
->with('scheduler.global_lock')
|
||||||
|
->once();
|
||||||
|
|
||||||
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
||||||
|
|
||||||
// Inject registry for testing
|
// Inject registry for testing
|
||||||
@@ -243,42 +257,43 @@ class SchedulerServiceTest extends TestCase
|
|||||||
->with('scheduler.global_lock', 1, 300)
|
->with('scheduler.global_lock', 1, 300)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
|
||||||
->with('scheduler.global_lock')
|
|
||||||
->once();
|
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.global_last_run', Mockery::type('int'))
|
->with('scheduler.global_last_run', Mockery::type('int'))
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
// Mock registry and job
|
// Mock registry and job
|
||||||
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
||||||
|
$registryMock->shouldReceive('loadJobsFromDatabase')->andReturnNull();
|
||||||
$jobMock = Mockery::mock(Job::class);
|
$jobMock = Mockery::mock(Job::class);
|
||||||
|
|
||||||
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
||||||
$jobMock->shouldReceive('getId')->andReturn('test_job_id');
|
$jobMock->shouldReceive('getId')->andReturn(42);
|
||||||
$jobMock->shouldReceive('isDue')->andReturn(true);
|
$jobMock->shouldReceive('isDue')->andReturn(true);
|
||||||
$jobMock->shouldReceive('run')->once();
|
$jobMock->shouldReceive('run')->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.lock.test_job_id')
|
->with('scheduler.lock.42')
|
||||||
->andReturn(null);
|
->andReturn(null);
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.last_run.test_job_id')
|
->with('scheduler.last_run.42')
|
||||||
->andReturn(null); // Never ran before
|
->andReturn(null); // Never ran before
|
||||||
|
|
||||||
// Lock and unlock operations
|
// Lock and unlock operations
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.lock.test_job_id', 1, 1800)
|
->with('scheduler.lock.42', 1, 1800)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
$this->cacheMock->shouldReceive('delete')
|
||||||
->with('scheduler.lock.test_job_id')
|
->with('scheduler.lock.42')
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.last_run.test_job_id', Mockery::type('int'))
|
->with('scheduler.last_run.42', Mockery::type('int'))
|
||||||
|
->once();
|
||||||
|
|
||||||
|
$this->cacheMock->shouldReceive('delete')
|
||||||
|
->with('scheduler.global_lock')
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
||||||
@@ -320,46 +335,43 @@ class SchedulerServiceTest extends TestCase
|
|||||||
->with('scheduler.global_lock', 1, 300)
|
->with('scheduler.global_lock', 1, 300)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
|
||||||
->with('scheduler.global_lock')
|
|
||||||
->once();
|
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.global_last_run', Mockery::type('int'))
|
->with('scheduler.global_last_run', Mockery::type('int'))
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
// Mock registry and job
|
// Mock registry and job
|
||||||
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
$registryMock = Mockery::mock(ScheduleJobRegistry::class);
|
||||||
|
$registryMock->shouldReceive('loadJobsFromDatabase')->andReturnNull();
|
||||||
$jobMock = Mockery::mock(Job::class);
|
$jobMock = Mockery::mock(Job::class);
|
||||||
|
|
||||||
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
$jobMock->shouldReceive('getName')->andReturn('TestJob');
|
||||||
$jobMock->shouldReceive('getId')->andReturn('test_job_id');
|
$jobMock->shouldReceive('getId')->andReturn(42);
|
||||||
$jobMock->shouldReceive('isDue')->andReturn(true);
|
$jobMock->shouldReceive('isDue')->andReturn(true);
|
||||||
$jobMock->shouldReceive('run')->andThrow(new \Exception('Job failed'));
|
$jobMock->shouldReceive('run')->andThrow(new \Exception('Job failed'));
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.lock.test_job_id')
|
->with('scheduler.lock.42')
|
||||||
->andReturn(null);
|
->andReturn(null);
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('get')
|
$this->cacheMock->shouldReceive('get')
|
||||||
->with('scheduler.last_run.test_job_id')
|
->with('scheduler.last_run.42')
|
||||||
->andReturn(null);
|
->andReturn(null);
|
||||||
|
|
||||||
// Lock and unlock operations
|
// Lock and unlock operations
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.lock.test_job_id', 1, 1800)
|
->with('scheduler.lock.42', 1, 1800)
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('delete')
|
$this->cacheMock->shouldReceive('delete')
|
||||||
->with('scheduler.lock.test_job_id')
|
->with('scheduler.lock.42')
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('set')
|
||||||
->with('scheduler.last_failure.test_job_id', Mockery::type('int'))
|
->with('scheduler.last_run.42', Mockery::type('int'))
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$this->cacheMock->shouldReceive('set')
|
$this->cacheMock->shouldReceive('delete')
|
||||||
->with('scheduler.last_failure_msg.test_job_id', 'Job failed')
|
->with('scheduler.global_lock')
|
||||||
->once();
|
->once();
|
||||||
|
|
||||||
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
$registryMock->shouldReceive('getJobs')->andReturn([$jobMock]);
|
||||||
|
|||||||
Reference in New Issue
Block a user