un( $post_id ) { $this->rollback_transaction(); if ( wp_cache_get( $post_id, 'posts' ) ) { $this->add_cache_compatibility_hooks(); /* * Our event report state would have been rolled back too, so try and reapply what was set locally. * Clear our cache, since it reflects local state and not aware of transaction rollbacks. */ try { clean_post_cache( $post_id ); } catch ( \Exception $e ) { // Some plugin intervening in the caching system did something wrong: we did what we could. } $this->remove_cache_compatibility_hooks(); } /** * Fires after a dry run migration was applied. * * @since 6.3.0 * * @param numeric $post_id The ID of the Event that was migrated. */ do_action( 'tec_events_custom_tables_v1_migration_after_dry_run', $post_id ); } /** * Add hooks to handle cache issues when we are clearing post cache during migration. * * @since 6.0.0 */ public function add_cache_compatibility_hooks() { add_filter( 'wpsc_delete_related_pages_on_edit', [ $this, 'wpsc_delete_related_pages_on_edit' ], 10, 1 ); } /** * Remove hooks to handle cache issues when we are clearing post cache during migration. * * @since 6.0.0 */ public function remove_cache_compatibility_hooks() { remove_filter( 'wpsc_delete_related_pages_on_edit', [ $this, 'wpsc_delete_related_pages_on_edit' ], 10 ); } /** * Skips some cache actions that fail in our cleanup of post cache. * * @since 6.0.0 * * @param mixed $all * * @return false */ public function wpsc_delete_related_pages_on_edit( $all ) { return false; } /** * Processes an Event migration. * * @since 6.0.0 * * @param int $post_id The post ID of the Event to migrate. * @param bool $dry_run Whether the migration should commit or just preview * the changes. * * @return Event_Report A reference to the migration report object produced by the * migration. */ public function migrate_event( int $post_id, bool $dry_run = false ): ?Event_Report { global $wpdb; // Log our worker starting do_action( 'tribe_log', 'debug', 'Worker: Migrate event:start', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'post_id' => $post_id, 'dry_run' => $dry_run ] ); $this->dry_run = $dry_run; /* * Get our Event_Report ready for the strategy. * This is also used in our error catching, so needs to be defined outside that block. */ $this->event_report = new Event_Report( get_post( $post_id ) ); if ( $this->check_phase() ) { // We're done, the migration is complete and there is no more work to do. return $this->event_report; } // Set our dead-man switch. $this->migration_completed = false; $this->bind_shutdown_handlers(); try { // Before we start preview, check if transactions are supported. // If not, we want to stop gracefully and still allow migration to continue. if ( $this->dry_run && ! $this->transactions_supported( $wpdb->prefix ) ) { // Clear all our workers, we don't need to check anymore for preview. tribe( Process::class )->empty_process_queue(); // Move us to the next phase - there will be a special message on that phase noting what happened. $this->state->set( 'phase', State::PHASE_MIGRATION_PROMPT ); $this->state->set( 'preview_unsupported', true ); $this->state->save(); $this->migration_completed = true; $this->unbind_shutdown_handlers(); return $this->event_report->migration_success(); } // In the odd scenario where we previously had a transaction failure, but it was resolved later. $this->state->set( 'preview_unsupported', false ); $this->state->save(); // Check if we are still in migration phase. if ( ! in_array( $this->state->get_phase(), [ State::PHASE_PREVIEW_IN_PROGRESS, State::PHASE_MIGRATION_IN_PROGRESS ], true ) ) { $this->event_report->migration_failed( 'canceled', [ $this->get_event_link_markup( $this->event_report->source_event_post->ID ) ] ); $this->migration_completed = true; $this->unbind_shutdown_handlers(); return $this->event_report; } /** * Filters the migration strategy that should be used to migrate an Event. * Returning an object implementing the TEC\Events\Custom_Tables\V1\Migration\Strategy_Interface * here will prevent TEC from using the default one. * * @since 6.0.0 * * @param Strategy_Interface A reference to the migration strategy that should be used. * Initially `null`. * @param int $post_id The post ID of the Event to migrate. * @param bool $dry_run Whether the strategy should be provided for a real migration * or its preview. */ $strategy = apply_filters( 'tec_events_custom_tables_v1_migration_strategy', null, $post_id, $dry_run ); if ( ! $strategy instanceof Strategy_Interface ) { $strategy = new Single_Event_Migration_Strategy( $post_id, $dry_run ); } // Set a strategy slug immediately; the strategy can refine it later. $strategy_class = get_class( $strategy ); $this->event_report->add_strategy( $strategy_class::get_slug() ); $this->event_report->start_event_migration(); // In case we have an error in the strategy, and we are forced to exit early, lets start the transaction here. if ( $this->dry_run ) { $this->before_dry_run( $post_id ); } $this->fix_event_meta( $post_id ); /** * Action to be fired immediately prior to applying migration strategy. Some migrations may still fail after this phase, * as there are various factors internal to the strategy that could cancel this migration. * * @since 6.0.0 * * @param Event_Report $event_report The event report for this migration. * @param Strategy_Interface $strategy The strategy being applied to this post. * @param numeric $post_id The post ID we are attempting to apply the migration to. * @param bool $dry_run Whether this is a dry run (preview) or a final migration being applied. */ do_action( 'tec_events_custom_tables_v1_before_migration_applied', $this->event_report, $strategy, $post_id, $dry_run ); // Apply strategy, use Event_Report to flag any pertinent details or any failure events. $strategy->apply( $this->event_report ); if ( $this->dry_run ) { $this->after_dry_run( $post_id ); } else { $this->transaction_commit(); } // If no error, mark successful. if ( ! $this->event_report->error ) { $this->event_report->migration_success(); } } catch ( Expected_Migration_Exception $e ) { if ( $this->dry_run ) { $this->after_dry_run( $post_id ); } $this->event_report->migration_failed( 'expected-exception', [ $e->getMessage(), ] ); } catch ( \Throwable $e ) { // In case we fail above, release transaction. if ( $this->dry_run ) { $this->after_dry_run( $post_id ); } $this->event_report->migration_failed( 'exception', [ $this->get_event_link_markup( $this->event_report->source_event_post->ID ), $e->getMessage(), '', '' ] ); // @todo Remove this. Useful for troubleshooting do_action( 'tribe_log', 'debug', 'Migration unexpected exception:', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'trace' => $e->getTraceAsString() ] ); } catch ( \Exception $e ) { // In case we fail above, release transaction. if ( $this->dry_run ) { $this->after_dry_run( $post_id ); } $this->event_report->migration_failed( 'exception', [ $this->get_event_link_markup( $this->event_report->source_event_post->ID ), $e->getMessage(), '', '' ] ); // @todo Remove this. Useful for troubleshooting do_action( 'tribe_log', 'debug', 'Migration unexpected exception:', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'trace' => $e->getTraceAsString() ] ); } $this->migration_completed = true; $this->unbind_shutdown_handlers(); // Flag to fail on first error. $fail_on_first_error = ( defined( 'TEC_EVENTS_CUSTOM_TABLES_V1_MIGRATION_STOP_ON_FAILURE' ) && TEC_EVENTS_CUSTOM_TABLES_V1_MIGRATION_STOP_ON_FAILURE ) || ! $dry_run; /** * Filter to determine whether we should stop on first failure or not. Useful for troubleshooting in preview mode. * @since 6.0.1 * * @param bool $fail_on_first_error * * @returns bool Whether we should stop on first failure or not. */ $fail_on_first_error = apply_filters( 'tec_events_custom_tables_v1_migration_should_stop_on_failure', $fail_on_first_error ); // If error in the migration phase or fail on first error flag, then we need to stop the queue. $did_migration_error = ( $fail_on_first_error && $this->event_report->error ); $continue_queue = $did_migration_error ? false : true; $next_post_id = null; if ( $continue_queue ) { // Get next event to process. $next_post_id = $this->events->get_id_to_process(); if ( $next_post_id ) { // Enqueue a new (Action Scheduler) action to import another Event. $action_id = as_enqueue_async_action( self::ACTION_PROCESS, [ $next_post_id, $dry_run ] ); if ( empty( $action_id ) ) { // If we cannot migrate the next Event we need to migrate, then the migration has failed. $this->event_report->migration_failed( "enqueue-failed", [ $this->get_event_link_markup( $this->event_report->source_event_post->ID ), $next_post_id, ] ); } } // Start a recursive check, but only if we are not already doing so. if ( ! as_has_scheduled_action( self::ACTION_CHECK_PHASE ) ) { $action_id = as_enqueue_async_action( self::ACTION_CHECK_PHASE ); if ( empty( $action_id ) ) { // The migration might have technically completed, but we cannot know for sure and will be conservative. $this->event_report->migration_failed( "check-phase-enqueue-failed", [ $this->get_event_link_markup( $this->event_report->source_event_post->ID ), ] ); } } } // If any error in migration phase, we need to stop and put back in a preview state for the user. if ( $did_migration_error ) { $this->cancel_migration_with_failure(); } else { $this->check_phase(); } // Do not hold a reference to the Report once the worker is done. $event_report = $this->event_report; $this->event_report = null; // Log our worker ending do_action( 'tribe_log', 'debug', 'Worker: Migrate event:end', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'post_id' => $post_id, 'next_post_id' => $next_post_id, 'dry_run' => $dry_run, 'event_report' => $event_report, ] ); return $event_report; } /** * Will trigger the migration failure handling. * * @since 6.0.0 */ public function cancel_migration_with_failure() { $process = tribe( Process::class ); $process->cancel_migration_with_failure(); } /** * Undoes an Event migration. * * @since 6.0.0 * * @param array The metadata we pass to ourselves. * */ public function undo_event_migration( $meta ) { // Log our worker starting do_action( 'tribe_log', 'debug', 'Worker: Undo event migration:start', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'meta' => $meta, ] ); if ( ! isset( $meta['started_timestamp'] ) ) { $meta['started_timestamp'] = time(); } $seconds_to_wait = 60 * 5; // 5 minutes $max_time_reached = ( time() - $meta['started_timestamp'] ) > $seconds_to_wait; // Are we still processing some events? If so, recurse and wait to do the undo operation. if ( ! $max_time_reached && $this->events->get_total_events_in_progress() ) { as_enqueue_async_action( self::ACTION_UNDO, [ $meta ] ); return; } $current_phase = $this->state->get_phase(); /* * Fires before the migration revert/cancellation starts. * * @since 6.0.0 */ do_action( 'tec_events_custom_tables_v1_migration_before_cancel' ); tribe( Schema_Builder::class )->down(); // Clear meta values. $meta_keys = [ Event_Report::META_KEY_MIGRATION_LOCK_HASH, ]; // If we are in migration failure, we want to preserve the report data. if ( $current_phase !== State::PHASE_MIGRATION_FAILURE_IN_PROGRESS ) { $meta_keys[] = Event_Report::META_KEY_REPORT_DATA; $meta_keys[] = Event_Report::META_KEY_MIGRATION_PHASE; $meta_keys[] = Event_Report::META_KEY_MIGRATION_CATEGORY; } /** * Filters the list of post meta keys to be removed during a migration cancel. * * @since 6.0.0 * * @param array $meta_keys List of keys to delete. */ $meta_keys = apply_filters( 'tec_events_custom_tables_v1_delete_meta_keys', $meta_keys ); foreach ( $meta_keys as $meta_key ) { delete_metadata( 'post', 0, $meta_key, '', true ); } // Setup success admin notice. switch ( $current_phase ) { case State::PHASE_CANCEL_IN_PROGRESS: case State::PHASE_REVERT_IN_PROGRESS: $is_cancel = $current_phase === State::PHASE_CANCEL_IN_PROGRESS; $text = tribe( String_Dictionary::class ); $notice = $text->get( $is_cancel ? 'cancel-migration-complete-notice' : 'revert-migration-complete-notice' ); Tribe__Admin__Notices::instance()->register_transient( 'admin_notice_undo_migration_complete', "

$notice

", [ 'type' => 'success', 'dismiss' => true, 'recurring' => true, ], MONTH_IN_SECONDS ); break; } // Which is our next phase? switch ( $current_phase ) { case State::PHASE_CANCEL_IN_PROGRESS: $next_phase = State::PHASE_CANCEL_COMPLETE; break; case State::PHASE_REVERT_IN_PROGRESS: $next_phase = State::PHASE_REVERT_COMPLETE; break; case State::PHASE_MIGRATION_FAILURE_IN_PROGRESS: $next_phase = State::PHASE_MIGRATION_FAILURE_COMPLETE; break; default: // Should not happen. // Graceful fallback. $next_phase = State::PHASE_PREVIEW_PROMPT; do_action( 'tribe_log', 'error', 'Worker: on undo, next phase not mapped.', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'meta' => $meta, 'phase' => $current_phase, ] ); break; } $this->state->set( 'complete_timestamp', time() ); $this->state->set( 'phase', $next_phase ); $this->state->save(); /** * Fires after the migration revert/cancellation has completed. * * @since 6.0.0 */ do_action( 'tec_events_custom_tables_v1_migration_after_cancel' ); // Log our worker ending do_action( 'tribe_log', 'debug', 'Worker: Undo event migration:end', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'meta' => $meta, ] ); } /** * Handles non-fatal errors that might be triggered during the migration. * * @since 6.0.0 * * @param int $errno The error code. * @param string $errstr The error message. * @param string $errfile The file the error occurred in. * * @return bool A value indicating whether the error handler handled the erorr or not.. * * @throws Migration_Exception A reference to an exception wrapping the error. */ public function error_handler( int $errno, string $errstr, string $errfile ): bool { $check_plugins = [ basename( TRIBE_EVENTS_FILE ) ]; if ( defined( 'EVENTS_CALENDAR_PRO_FILE' ) ) { $check_plugins[] = basename( EVENTS_CALENDAR_PRO_FILE ); } if ( ! tec_is_file_from_plugins( $errfile, ...$check_plugins ) ) { // Do not handle Warnings when coming from outside TEC or ECP codebase (e.g. caching plugins). return false; } // Delegate to our try/catch handler. throw new Migration_Exception( $errstr, $errno ); } /** * Hooked to the WordPress `shutdown` hook. * This method should be removed during a successful migration * or one that is properly handled. If not, then this method is * an attempt to log the failure. * * @since 6.0.0 */ public function shutdown_handler() { // In case we fail above, release transaction. if ( $this->dry_run ) { $this->transaction_rollback(); } $event_link_markup = $this->get_event_link_markup( $this->event_report->source_event_post->ID ); // If we're here, the migration failed. $this->event_report->migration_failed( "unknown-shutdown", [ $event_link_markup, '', '' ] ); } /** * Checks and updates the migration phase depending on the current status of the database. * * This is an idem-potent method that will only ste the migration state to done * when done; two or more concurrent workers doing the same will not break the * logic. * * @since 6.0.0 * * @return bool Whether the migration, or its preview, is completed or not. */ public function check_phase() { $state = tribe( State::class ); do_action( 'tribe_log', 'debug', 'Worker: Migrate event:check_phase', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'phase' => $state->get_phase(), ] ); $phase = $state->get_phase(); $migration_completed = in_array( $phase, [ State::PHASE_MIGRATION_IN_PROGRESS, State::PHASE_PREVIEW_IN_PROGRESS ], true ) && $this->events->get_total_events_remaining() === 0 && $this->events->get_total_events_in_progress() === 0; if ( ! $migration_completed ) { do_action( 'tribe_log', 'debug', 'Worker: Migrate event:check_phase', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, ] ); return false; } $next_phase = $phase === State::PHASE_PREVIEW_IN_PROGRESS ? State::PHASE_MIGRATION_PROMPT : State::PHASE_MIGRATION_COMPLETE; $state->set( 'phase', $next_phase ); $state->set( 'migration', 'estimated_time_in_seconds', $this->events->calculate_time_to_completion() ); $state->set( 'complete_timestamp', time() ); $state->save(); /** * Triggers an action on the end of the Migration. * * @since 6.0.0 * * @param bool $dry_run Whether the migration just completed is a dry-run one or not. */ do_action( 'tec_events_custom_tables_v1_migration_completed', $this->dry_run ); do_action( 'tribe_log', 'debug', 'Worker: Migrate event:check_phase', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'phase' => $state->get_phase(), ] ); return true; } /** * Hooked to the `ob_start` function, this method will run consistently * across PHP versions when the `die` or `exit` function is called during * the migration process. * * @since 6.0.0 * * @param string $buffer A string buffer that will contain all the output * produced by the PHP code before the `die` or `exit` * call. */ public function ob_flush_handler( $buffer ) { if ( $this->migration_completed ) { // If we set the switch flag, then we already handled possible errors. return; } // In case we fail above, release transaction. if ( $this->dry_run ) { $this->transaction_rollback(); } $event_link_markup = $this->get_event_link_markup( $this->event_report->source_event_post->ID ); /** * Since we're storing output of arbitrary length in the database, let's * trim it down to something that should not go over the `mysql_max_packet` * size. */ $trimmed_buffer = substr( $buffer, 0, 1024 ); // If we're here, some code called `die` or `exit`. $this->event_report->migration_failed( 'exit', [ $event_link_markup, $trimmed_buffer, '', '' ] ); /* * This method might be the last executing before a hard `die` or `exit` call, let's check the phase. * If we could not queue further actions to process more Events or check the phase, let's do it now. */ $this->check_phase(); } /** * Checks if the current phase is completed or not, * else queue another action to run the same check. * * @since 6.0.0 * * @return int The ID of the new Action scheduled to check * on the migration phase, `0` if no new Action * was queued. */ public function check_phase_complete() { $completed = ! $this->state->is_running() || $this->check_phase(); do_action( 'tribe_log', 'debug', 'Worker: Check event:check_phase_complete', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, 'phase' => $this->state->get_phase(), 'completed' => $completed ] ); if ( $completed ) { do_action( 'tribe_log', 'debug', 'Worker: Check event:check_phase_complete', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, ] ); // Clear all of our queued state check workers. as_unschedule_all_actions( self::ACTION_CHECK_PHASE ); return 0; } // We may already have one scheduled, bail if we do. if ( as_has_scheduled_action( self::ACTION_CHECK_PHASE ) ) { do_action( 'tribe_log', 'debug', 'Worker: Check event:check_phase_complete', [ 'source' => __CLASS__ . ' ' . __METHOD__ . ' ' . __LINE__, ] ); return 0; } // Check again. return as_enqueue_async_action( self::ACTION_CHECK_PHASE ); } /** * Start a transaction with fallback on no-op queries if not supported. * * @since 6.0.0 */ private function start_transaction() { $this->transaction_started = $this->transaction_start(); if ( ! $this->transaction_started ) { // Transactions might be not supported or blocked: do not actually execute queries. Builder::class_enable_query_execution( false ); } } /** * Rolls back a transaction with fallback on no-op queries if not supported. * * @since 6.0.0 */ private function rollback_transaction() { if ( $this->transaction_started ) { $this->transaction_rollback(); } else { Builder::class_enable_query_execution( true ); } } /** * Updates the Event date and duration meta to make sure it's consistent. * * @since 6.0.1 * * @param int $post_id The ID of the Event to update. * * @return void Updates the Event date and duration meta to make sure it's consistent. * * @throws Migration_Exception If the Event date and duration meta could not be updated. */ private function fix_event_meta( int $post_id ): void { /** * Filters whether an Event date related meta should be fixed before migration or not. * * @since 6.0.0 * * @param bool $fix_event_duration Whether the Event date related meta should be fixed before migration or not. * @param int $post_id The ID of the post being migrated. */ $should_fix_meta = apply_filters( 'tec_events_custom_tables_v1_migration_fix_event_date_meta', true, $post_id ); if ( ! $should_fix_meta ) { return; } // At this stage, we can be sure the meta will be there. $start_date = get_post_meta( $post_id, '_EventStartDate', true ); $end_date = get_post_meta( $post_id, '_EventEndDate', true ); $start_date_utc = get_post_meta( $post_id, '_EventStartDateUTC', true ); $end_date_utc = get_post_meta( $post_id, '_EventEndDateUTC', true ); $has_start = ! empty( $start_date ) || ! empty( $start_date_utc ); $has_end = ! empty( $end_date ) || ! empty( $end_date_utc ); if ( ! ( $has_start && $has_end ) ) { throw new Migration_Exception( 'Required Event date data is missing: check the event for missing or invalid data in the start and end date fields.' ); } $timezone_string = get_post_meta( $post_id, '_EventTimezone', true ); if ( ! Timezones::is_valid_timezone( $timezone_string ) ) { // Use the site one, if not set. $timezone_string = Timezones::build_timezone_object()->getName(); update_post_meta( $post_id, '_EventTimezone', $timezone_string ); update_post_meta( $post_id, '_EventTimezoneAbbr', Timezones::abbr( $start_date, $timezone_string ) ); } $timezone = Timezones::build_timezone_object( $timezone_string ); $utc = new \DateTimeZone( 'UTC' ); $dtstart = $start_date ? Dates::immutable( $start_date, $timezone ) : Dates::immutable( $start_date_utc, $utc )->setTimezone( $timezone ); $dtend = $end_date ? Dates::immutable( $end_date, $timezone ) : Dates::immutable( $end_date_utc, $utc )->setTimezone( $timezone ); $updated_duration = $dtend->getTimestamp() - $dtstart->getTimestamp(); $event_start_date = $dtstart->format( Dates::DBDATETIMEFORMAT ); $event_end_date = $dtend->format( Dates::DBDATETIMEFORMAT ); $event_start_date_utc = $dtstart->setTimezone( $utc )->format( Dates::DBDATETIMEFORMAT ); $event_end_date_utc = $dtend->setTimezone( $utc )->format( Dates::DBDATETIMEFORMAT ); update_post_meta( $post_id, '_EventDuration', $updated_duration ); update_post_meta( $post_id, '_EventStartDate', $event_start_date ); update_post_meta( $post_id, '_EventEndDate', $event_end_date ); update_post_meta( $post_id, '_EventStartDateUTC', $event_start_date_utc ); update_post_meta( $post_id, '_EventEndDateUTC', $event_end_date_utc ); } /** * Migrates up to a number of not yet migrated Events. * * @since 6.0.2 * * @param int $count The number of Events to migrate, at the most. * * @return int The number of migrated Events. */ public function migrate_many_events( int $count ): int { if ( $count <= 0 ) { return 0; } $free_ids = $this->events->get_ids_to_process( $count ); $dry_run = $this->state->is_dry_run(); $migrated = 0; if ( count( $free_ids ) > 0 ) { // We might have less free ids than we want but have some, roll with it. foreach ( $free_ids as $post_id ) { $report = $this->migrate_event( $post_id, $dry_run ); $migrated ++; if ( ! ( $report instanceof Event_Report && $report->status === 'success' ) ) { // We have an error, stop here. break; } } return $migrated; } // We have no free ids, let's see if we can grab some from the Action Scheduler actions. $actions = as_get_scheduled_actions( [ 'hook' => self::ACTION_PROCESS, 'status' => ActionScheduler_Store::STATUS_PENDING, 'per_page' => $count, ] ); if ( count( $actions ) === 0 ) { // We have no pending actions, we're done. return $migrated; } /** * We don't want to trigger cancellation steps - we are still processing, just taking out of queue. */ remove_action( 'action_scheduler_canceled_action', [ tribe( Provider::class ), 'cancel_async_action' ] ); /** @var \ActionScheduler_Action $action */ foreach ( $actions as $action ) { // Unschedule a pending action to migrate the Event now. $hook = $action->get_hook(); $args = $action->get_args(); $group = $action->get_group(); $unscheduled = as_unschedule_action( $hook, $args, $group ); if ( empty( $unscheduled ) ) { // The action might have been executed in the meantime, skip it. continue; } $report = $this->migrate_event( ...$args ); $migrated ++; if ( ! ( $report instanceof Event_Report && $report->status === 'success' ) ) { // We have an error, stop here. break; } } return $migrated; } } Autres services - LA CASA Café Culturel
Aller au contenu

Autres services

Nous proposons trois volets de services complémentaires aux particuliers et aux professionnels. Contactez-nous pour une offre sur mesure selon vos besoins et votre demande !

Sous-location de salle

À la recherche d’une salle pour un stage, réunion, cours particulier ou cours à l’année ? Nous mettons à votre disposition notre salle de 35m2 avec miroir et sono.

Traiteur en entreprise

Pour une réunion professionnelle, séminaire ou evenement, nous vous proposons un service de catering avec nos meilleurs produits.

Créations chorégraphiques

Une idée d’action artistique en lien avec la danse ? Nos artistes peuvent vous apporter leur savoir-faire en direction artistique pour vos événements promotionnels et autres actions de communication.

Pour toute demande de devis et renseignements complémentaires : lacasaperols@gmail.com