rows = $rows; $this->build = $build; $this->facts = $facts; $this->channel = $channel; $this->switches = $switches; $this->log = $log; } /** * Checks one listing and updates its advert when the shop item changed. * * @param int $id Listing ID. * @param int $failures Failed attempts so far. */ public function check( int $id, int $failures = 0 ): UpdateResult { $row = $this->rows->find( $id ); if ( null === $row || '' === (string) $row['remote_id'] || ! in_array( $row['status'], ListingStatus::on_marketplace(), true ) ) { return UpdateResult::of( UpdateResult::NOT_LISTED ); } if ( '' !== TakedownRules::reason( (array) call_user_func( $this->facts, $row ) ) ) { return UpdateResult::of( UpdateResult::TAKEDOWN_NEEDED ); } $result = call_user_func( $this->build, $row ); if ( ! $result instanceof BuildResult ) { return UpdateResult::of( UpdateResult::TAKEDOWN_NEEDED ); // The item is gone. } if ( ! $result->is_ready() ) { $this->rows->update( $id, array( 'last_error' => 'Not updated: ' . $result->errors[0]['text'] ) ); return UpdateResult::of( UpdateResult::BLOCKED ); } if ( $result->hash() === (string) $row['payload_hash'] ) { if ( '' !== (string) $row['last_error'] ) { $this->rows->update( $id, array( 'last_error' => '' ) ); } return UpdateResult::of( UpdateResult::UNCHANGED ); } if ( $this->switches->is_paused() ) { return UpdateResult::of( UpdateResult::RETRY, self::DELAY_PAUSED ); } // Something changed. Before sending it, take the category the advert has now, in case the owner changed it there by hand. $fresh = $this->with_current_category( $row ); if ( (int) ( $fresh['remote_category'] ?? 0 ) !== (int) ( $row['remote_category'] ?? 0 ) ) { $row = $fresh; $result = call_user_func( $this->build, $row ); if ( ! $result instanceof BuildResult ) { return UpdateResult::of( UpdateResult::TAKEDOWN_NEEDED ); } if ( ! $result->is_ready() ) { $this->rows->update( $id, array( 'last_error' => 'Not updated: ' . $result->errors[0]['text'] ) ); return UpdateResult::of( UpdateResult::BLOCKED ); } } try { $outcome = $this->channel->update( (string) $row['remote_id'], $result->listing ); } catch ( BlockedException $e ) { $this->log->log( Logger::WARNING, 'update', sprintf( 'Update of listing %d is waiting: %s', $id, $e->getMessage() ) ); return UpdateResult::of( UpdateResult::RETRY, self::DELAY_PAUSED ); } catch ( AuthException $e ) { $this->log->log( Logger::ERROR, 'update', sprintf( 'Update of listing %d waits for the connection: %s', $id, $e->getMessage() ) ); return UpdateResult::of( UpdateResult::RETRY, self::DELAY_AUTH ); } catch ( TransportException $e ) { $this->log->log( Logger::WARNING, 'update', sprintf( 'Update of listing %d: no connection (%s).', $id, $e->getMessage() ) ); return UpdateResult::of( UpdateResult::RETRY, self::DELAY_NETWORK ); } catch ( ChannelException $e ) { if ( $e instanceof ApiException && in_array( $e->status(), array( 403, 429 ), true ) ) { return UpdateResult::of( UpdateResult::RETRY, self::DELAY_PAUSED ); } return $this->failed( $id, $failures, ErrorExplainer::for_channel( $this->channel )->explain_exception( $e ) ); } if ( $outcome->simulated ) { $this->log->log( Logger::INFO, 'update', sprintf( 'Dry-run: advert %s of listing %d would be updated now.', (string) $row['remote_id'], $id ), $result->listing->to_array() ); return UpdateResult::of( UpdateResult::SIMULATED ); } $fields = $outcome->fields(); $fields['payload_hash'] = $result->hash(); $fields['last_synced_at'] = gmdate( 'Y-m-d H:i:s' ); $fields['last_error'] = ''; $this->rows->update( $id, $fields ); $this->log->log( Logger::INFO, 'update', sprintf( 'Advert %s of listing %d updated.', (string) $row['remote_id'], $id ) ); return UpdateResult::of( UpdateResult::UPDATED ); } /** * Reads the category the advert is in at the marketplace now (read-only) and stores it, so a category the owner * changed there by hand is not put back by this update. If the marketplace cannot be read the stored one is used. * * @param array $row Listing row. */ private function with_current_category( array $row ): array { try { $category = $this->channel->status( (string) $row['remote_id'] )->remote_category; } catch ( ChannelException $e ) { return $row; } if ( $category > 0 && (int) ( $row['remote_category'] ?? 0 ) !== $category ) { $this->rows->update( (int) $row['id'], array( 'remote_category' => $category ) ); $row['remote_category'] = $category; } return $row; } /** * The marketplace refused the update: try again later, and give up after MAX_ATTEMPTS. * * @param int $id Listing ID. * @param int $failures Failed attempts before this one. * @param string $message Error. */ private function failed( int $id, int $failures, string $message ): UpdateResult { $this->log->log( Logger::ERROR, 'update', sprintf( 'Update of listing %d failed (attempt %d): %s', $id, $failures + 1, $message ) ); if ( $failures + 1 >= self::MAX_ATTEMPTS ) { $this->rows->update( $id, array( 'last_error' => 'Automatic update failed: ' . $message ) ); return UpdateResult::of( UpdateResult::GAVE_UP ); } return UpdateResult::of( UpdateResult::RETRY, self::DELAY_ERROR, true ); } }