<?php
namespace App\Entity\Profile;
use App\Entity\ConversationMessage\ConversationMessage;
use App\Entity\ExternalPartner\IntegratedExternalPartnerCustomer;
use App\Entity\JobseekerOccupationalFieldCapabilityValue;
use App\Entity\JobseekerProfileAdditionalFile;
use App\Entity\OccupationalField;
use App\Entity\Profile;
use App\Entity\ProfileBlock;
use App\Entity\ProfileFavorization;
use App\Entity\ProfileReview;
use App\Entity\User;
use App\Entity\WantedJob;
use App\Service\EntityCacheService;
use App\Service\OccupationalFieldCapabilitiesService;
use App\Utility\TextCleaner;
use App\Validator\Constraint as AppAssert;
use App\Value\MimeTypes;
use App\Value\ZipcodeRadiusesValue;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Exception;
use InvalidArgumentException;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity
*
* @ORM\Table(name="jobseeker_profiles")
*/
class JobseekerProfile extends Profile
{
public const POPULAR_SEARCHTERMS = [
'Helfer',
'Fahrer PKW/LKW',
'Lagerhelfer',
'Pflegekraft',
'Kassenkraft',
'Reinigungskraft',
'Sicherheitsdienst',
'Verkäufer',
'Vertriebler',
'Hausmeister',
'Kaufmann/-frau',
'Sachbearbeiter',
'Service/Kellner',
'Call-Center',
'Empfang/Rezeption',
'Büroassistent',
'Küchenhilfe',
'Kundenberater',
'Handwerkshelfer',
'Monteur',
'Influencer',
'Garten-Landschaftsbau'
];
public function __construct()
{
parent::__construct();
$this->occupationalFieldCapabilityValues = new ArrayCollection();
$this->wantedJobs = new ArrayCollection();
$this->additionalFiles = new ArrayCollection();
$this->availabilityRadius = ZipcodeRadiusesValue::JOBSEEKER_PROFILE_DEFAULT;
$this->experience = self::EXPERIENCE_MORE_THAN_ONE_YEAR;
}
/**
* @var User
*
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="jobseekerProfiles", cascade={"persist"})
*
* @ORM\JoinColumn(name="users_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")
*/
protected $user;
/**
* @var string
*
* @ORM\Column(name="selfdescription", type="text", length=10000, nullable=true)
*/
protected $selfdescription;
public function setSelfdescription(?string $selfdescription = null): void
{
$this->selfdescription = $selfdescription;
}
public function getSelfdescription(): ?string
{
return TextCleaner::removeSpecialCharacters($this->selfdescription);
}
/**
* @var string
*
* @ORM\Column(name="firstname", type="string", length=128, nullable=true)
*
* @Assert\NotBlank()
*
* @Assert\Length(
* min = 2,
* max = 50,
* )
*/
protected $firstname;
public function setFirstname(?string $firstname = null): void
{
$this->firstname = $firstname;
}
public function getFirstname(): ?string
{
return $this->firstname;
}
/**
* @var string
*
* @ORM\Column(name="lastname", type="string", length=128, nullable=true)
*
* @Assert\Length(
* min = 0,
* max = 50,
* )
*/
protected $lastname;
public function setLastname(?string $lastname = null): void
{
$this->lastname = $lastname;
}
public function getLastname(): ?string
{
return $this->lastname;
}
/**
* @var string
*
* @ORM\Column(name="address", type="string", length=128, nullable=true)
*
* @Assert\Length(
* min = 0,
* max = 128,
* )
*/
protected $address;
public function setAddress(?string $address = null): void
{
$this->address = $address;
}
public function getAddress(): ?string
{
return $this->address;
}
/**
* @var string
*
* @ORM\Column(name="zipcode", type="string", length=128, nullable=true)
*
* @AppAssert\KnownZipcode
*/
protected $zipcode;
public function setZipcode(?string $zipcode = null): void
{
if (!is_null($zipcode)) {
$trimmedZipcode = trim($zipcode);
if (mb_strlen($trimmedZipcode) !== 5 || !is_numeric($trimmedZipcode)) {
throw new InvalidArgumentException("zipcode must either be null or a string with 5 digits, but got '$zipcode'");
}
$zipcode = $trimmedZipcode;
}
$this->zipcode = $zipcode;
}
public function getZipcode(): ?string
{
return $this->zipcode;
}
/**
* @var string
*
* @ORM\Column(name="city", type="string", length=128, nullable=true)
*
* @Assert\Length(
* min = 0,
* max = 128,
* )
*/
protected $city;
public function setCity(?string $city = null): void
{
$this->city = mb_substr($city, 0, 128);
}
public function getCity(): ?string
{
return $this->city;
}
/**
* @var ConversationMessage[]|Collection|array
*
* @ORM\OneToMany(targetEntity="\App\Entity\ConversationMessage\ConversationMessage", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*/
protected $conversationMessages;
public function addConversationMessage(ConversationMessage $conversationMessage): void
{
$this->conversationMessages[] = $conversationMessage;
}
/** @return ConversationMessage[]|Collection|array */
public function getConversationMessages(): Collection
{
return $this->conversationMessages;
}
/**
* @var ProfileReview|Collection
*
* @ORM\OneToMany(targetEntity="\App\Entity\ProfileReview", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*/
protected $profileReviews;
public function addProfileReview(ProfileReview $profileReview): void
{
$this->profileReviews[] = $profileReview;
}
public function getProfileReviews()
{
return $this->profileReviews;
}
/**
* @var bool
*
* @ORM\Column(name="mobilenumber_public", type="boolean", nullable=true)
*/
protected $mobilenumberPublic;
public function getMobilenumberPublic(): ?bool
{
return $this->mobilenumberPublic;
}
public function setMobilenumberPublic(?bool $mobilenumberPublic): void
{
$this->mobilenumberPublic = $mobilenumberPublic;
}
/**
* @ORM\Column(name="whatsapp_allowed", type="boolean", nullable=true)
*/
protected ?bool $whatsappAllowed = null;
public function getWhatsappAllowed(): ?bool
{
return $this->whatsappAllowed;
}
public function setWhatsappAllowed(?bool $whatsappAllowed): void
{
$this->whatsappAllowed = $whatsappAllowed;
}
/**
* @var ProfileBlock|Collection
*
* @ORM\OneToMany(targetEntity="\App\Entity\ProfileBlock", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*/
protected $profileBlocks;
public function addProfileBlock(ProfileBlock $profileBlock): void
{
$this->profileBlocks[] = $profileBlock;
}
public function getProfileBlocks()
{
return $this->profileBlocks;
}
/** @throws Exception */
public function getProfileBlockForBlockOfJoboffererProfile(JoboffererProfile $joboffererProfile): ProfileBlock
{
/** @var ProfileBlock $profileBlock */
foreach ($this->profileBlocks as $profileBlock) {
if ($profileBlock->isBlocker($this) && !is_null($profileBlock->getJoboffererProfile()) && $profileBlock->getJoboffererProfile()->getId() === $joboffererProfile->getId()) {
return $profileBlock;
}
}
throw new Exception('Jobseeker profile ' . $this->getId() . ' has not blocked jobofferer profile ' . $joboffererProfile->getId());
}
/** @throws Exception */
public function getProfileBlockForBlockOfCustomer(IntegratedExternalPartnerCustomer $customer): ProfileBlock
{
/** @var ProfileBlock $profileBlock */
foreach ($this->profileBlocks as $profileBlock) {
if ($profileBlock->isBlocker($this) && !is_null($profileBlock->getIntegratedExternalPartnerCustomer()) && $profileBlock->getIntegratedExternalPartnerCustomer()->getId() === $customer->getId()) {
return $profileBlock;
}
}
throw new Exception('Jobseeker profile ' . $this->getId() . ' has not blocked jobofferer profile ' . $customer->getId());
}
/**
* @var ProfileFavorization|Collection
*
* @ORM\OneToMany(targetEntity="\App\Entity\ProfileFavorization", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*/
protected $profileFavorizations;
public function addProfileFavorization(ProfileFavorization $profileFavorization): void
{
$this->profileFavorizations[] = $profileFavorization;
}
public function getProfileFavorizations()
{
return $this->profileFavorizations;
}
/** @throws Exception */
public function getProfileFavorizationForFavorizationOfJoboffererProfile(JoboffererProfile $joboffererProfile): ProfileFavorization
{
/** @var ProfileFavorization $profileFavorization */
foreach ($this->profileFavorizations as $profileFavorization) {
if ($profileFavorization->isFavorer($this) && $profileFavorization->getJoboffererProfile()->getId() === $joboffererProfile->getId()) {
return $profileFavorization;
}
}
throw new Exception('Jobseeker profile ' . $this->getId() . ' has not favored jobofferer profile ' . $joboffererProfile->getId());
}
/**
* @var JobseekerOccupationalFieldCapabilityValue
*
* @ORM\OneToMany(targetEntity="\App\Entity\JobseekerOccupationalFieldCapabilityValue", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*/
protected $occupationalFieldCapabilityValues;
/** @return JobseekerOccupationalFieldCapabilityValue[]|Collection */
public function getOccupationalFieldCapabilityValues(): Collection
{
return $this->occupationalFieldCapabilityValues;
}
public function hasAtLeastOneOccupationalFieldCapabilityAboveZero(?EntityCacheService $entityCacheService = null, string $entityCacheContext = ''): bool
{
$occupationalFieldCapabilityValues = null;
if (!is_null($entityCacheService)) {
$occupationalFieldCapabilityValues = $entityCacheService->getEntitiesFromCacheForEntityClassNameByField(
JobseekerOccupationalFieldCapabilityValue::class,
'jobseekerProfile',
$this->getId(),
$entityCacheContext
);
}
if (is_null($occupationalFieldCapabilityValues)) {
$occupationalFieldCapabilityValues = $this->occupationalFieldCapabilityValues;
}
/** @var JobseekerOccupationalFieldCapabilityValue $occupationalFieldCapabilityValue */
foreach ($occupationalFieldCapabilityValues as $occupationalFieldCapabilityValue) {
if ($occupationalFieldCapabilityValue->getValue() > 0) {
return true;
}
}
return false;
}
public function getAllOccupationalFieldIdsToCapabilityIdsToValuesSlim(): array
{
$result = [];
foreach (OccupationalFieldCapabilitiesService::CAPABILITY_IDS_BY_OCCUPATIONAL_FIELD_IDS as $occupationalFieldId => $capabilityIds) {
foreach ($capabilityIds as $capabilityId) {
$value = 0;
/** @var JobseekerOccupationalFieldCapabilityValue $occupationalFieldCapabilityValue */
foreach ($this->occupationalFieldCapabilityValues as $occupationalFieldCapabilityValue) {
if ($occupationalFieldCapabilityValue->getCapabilityId() === $capabilityId) {
$value = $occupationalFieldCapabilityValue->getValue();
}
}
$result[$capabilityId] = $value;
}
}
return $result;
}
public function getAllOccupationalFieldIdsToCapabilityIdsToValuesSlimIncludeLegacy(): array
{
$result = [];
$capabilities = OccupationalFieldCapabilitiesService::CAPABILITY_IDS_BY_OCCUPATIONAL_FIELD_IDS;
array_push($capabilities[0], OccupationalFieldCapabilitiesService::CAPABILITY_ID_GENERAL_LEGACY);
foreach ($capabilities as $occupationalFieldId => $capabilityIds) {
foreach ($capabilityIds as $capabilityId) {
$value = 0;
/** @var JobseekerOccupationalFieldCapabilityValue $occupationalFieldCapabilityValue */
foreach ($this->occupationalFieldCapabilityValues as $occupationalFieldCapabilityValue) {
if ($occupationalFieldCapabilityValue->getCapabilityId() === $capabilityId) {
$value = $occupationalFieldCapabilityValue->getValue();
}
}
$result[$capabilityId] = $value;
}
}
return $result;
}
/**
* @var WantedJob[]|Collection
*
* @ORM\OneToMany(targetEntity="\App\Entity\WantedJob", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*/
protected $wantedJobs;
public function addWantedJob(WantedJob $wantedJob): void
{
$this->wantedJobs[] = $wantedJob;
}
/**
* @return WantedJob[]|Collection
*/
public function getWantedJobs(): Collection
{
return $this->wantedJobs;
}
/**
* @var OccupationalField|Collection
*
* @ORM\ManyToMany(targetEntity="App\Entity\OccupationalField", inversedBy="jobseekerProfiles", cascade={"persist"})
*
* @ORM\JoinTable(
* name="jobseeker_profiles_occupational_fields",
* joinColumns={
*
* @ORM\JoinColumn(name="jobseeker_profile_id", referencedColumnName="id", onDelete="CASCADE")
* },
* inverseJoinColumns={
* @ORM\JoinColumn(name="occupational_field_id", referencedColumnName="id", onDelete="CASCADE")
* }
* )
*
* @Assert\Type("\Doctrine\Common\Collections\Collection")
*/
protected $occupationalFields;
public function setOccupationalFields(Collection $occupationalFields)
{
$this->occupationalFields = $occupationalFields;
}
/**
* @return Collection|OccupationalField[]
*/
public function getOccupationalFields(?EntityCacheService $entityCacheService = null, string $entityCacheContext = ''): Collection
{
$occupationalFields = null;
if (!is_null($entityCacheService)) {
$occupationalFields = $entityCacheService->getEntitiesFromCacheForEntityClassNameByField(
OccupationalField::class,
'jobseekerProfile',
$this->getId(),
$entityCacheContext
);
}
if (is_null($occupationalFields)) {
return $this->occupationalFields;
} else {
return new ArrayCollection($occupationalFields);
}
}
/**
* @var JobseekerProfileAdditionalFile|Collection
*
* @ORM\OneToMany(targetEntity="App\Entity\JobseekerProfileAdditionalFile", mappedBy="jobseekerProfile", cascade={"persist", "remove"})
*
* @Assert\Count(min=0,max=10)
*
* @Assert\Type("\Doctrine\Common\Collections\Collection")
*/
protected $additionalFiles;
public function setAdditionalFiles(Collection $additionalFiles): void
{
$this->additionalFiles = $additionalFiles;
}
public function getAdditionalFiles(): Collection
{
if (is_null($this->additionalFiles)) {
return new ArrayCollection();
} else {
return $this->additionalFiles;
}
}
/**
* @var string
*
* @ORM\Column(name="availability_zipcode", type="string", length=5, nullable=true)
*
* @Assert\Type("string")
*
* @Assert\NotNull()
*
* @Assert\NotBlank()
*
* @AppAssert\KnownZipcode
*/
protected $availabilityZipcode;
public function setAvailabilityZipcode(?string $availabilityZipcode = null): void
{
if (!is_null($availabilityZipcode)) {
$trimmedZipcode = trim($availabilityZipcode);
if (mb_strlen($trimmedZipcode) !== 5 || !is_numeric($trimmedZipcode)) {
throw new InvalidArgumentException("zipcode must either be null or a string with 5 digits, but got '$availabilityZipcode'");
}
$availabilityZipcode = $trimmedZipcode;
}
$this->availabilityZipcode = $availabilityZipcode;
}
public function getAvailabilityZipcode(): ?string
{
return $this->availabilityZipcode;
}
/**
* @var int
*
* @ORM\Column(name="availability_radius", type="integer", nullable=false)
*
* @Assert\Type("int")
*/
protected $availabilityRadius;
public function getLegacyAvailabilityRadius(): int
{
return $this->availabilityRadius;
}
/**
* @var int
*
* @ORM\Column(name="experience", type="smallint", nullable=false)
*
* @Assert\Type("int")
*/
protected $experience;
public const EXPERIENCE_NONE = 0;
public const EXPERIENCE_MORE_THAN_ONE_YEAR = 1;
public const EXPERIENCE_MORE_THAN_THREE_YEARS = 3;
public const EXPERIENCE_MORE_THAN_FIVE_YEARS = 5;
// This may only grow, never remove existing entries because it makes data already in the db invalid!
// Change POSSIBLE_EXPERIENCES_AVAILABLE_FOR_SELECTION instead.
public const POSSIBLE_EXPERIENCES = [
self::EXPERIENCE_NONE,
self::EXPERIENCE_MORE_THAN_ONE_YEAR,
self::EXPERIENCE_MORE_THAN_THREE_YEARS,
self::EXPERIENCE_MORE_THAN_FIVE_YEARS,
];
public const POSSIBLE_EXPERIENCES_AVAILABLE_FOR_SELECTION = [
self::EXPERIENCE_NONE,
self::EXPERIENCE_MORE_THAN_ONE_YEAR,
self::EXPERIENCE_MORE_THAN_THREE_YEARS
];
public const POSSIBLE_EXPERIENCES_AVAILABLE_FOR_SELECTION_WITH_TRANSLATION_MAPPING = [
'profiles.jobseeker.experience_none' => self::EXPERIENCE_NONE,
'profiles.jobseeker.experience_more_than_one_year' => self::EXPERIENCE_MORE_THAN_ONE_YEAR,
'profiles.jobseeker.experience_more_than_three_years' => self::EXPERIENCE_MORE_THAN_THREE_YEARS
];
// This needs to contain ALL translations, even for entries that are not available in the frontend,
// because the db can contain entries with an experience value that is no longer offered, but must
// still be presented in the frontend
public const POSSIBLE_EXPERIENCES_WITH_TRANSLATION_MAPPING_REVERSE = [
self::EXPERIENCE_NONE => 'profiles.jobseeker.experience_none',
self::EXPERIENCE_MORE_THAN_ONE_YEAR => 'profiles.jobseeker.experience_more_than_one_year',
self::EXPERIENCE_MORE_THAN_THREE_YEARS => 'profiles.jobseeker.experience_more_than_three_years',
self::EXPERIENCE_MORE_THAN_FIVE_YEARS => 'profiles.jobseeker.experience_more_than_five_years'
];
public function getLegacyExperience(): int
{
return $this->experience;
}
/**
* @Assert\Callback
*
* We have to handle two stages of validation here. If the user has not yet created a valid "base profile"
* consisting of firstname, occupational fields, availabilities, availabilityZipcode, and zipcodeCircumcircle,
* then we do not yet validate the fields for the "extended profile" like lastname, description, address etc.
*/
public function validate(ExecutionContextInterface $context, $payload): void
{
parent::validate($context, $payload);
/** @var JobseekerProfileAdditionalFile $additionalFile */
foreach ($this->additionalFiles as $additionalFile) {
if (!is_null($additionalFile->getFile()) && $additionalFile->getFile() != '') {
if (!in_array($additionalFile->getFile()->getMimeType(), MimeTypes::ALLOWED_FOR_USER_UPLOAD_ALL)) {
$context
->buildViolation('profiles.editor_page.additional_file_filetype_not_allowed')
->setTranslationDomain('messages')
->atPath('additionalFiles')
->addViolation();
}
if ($additionalFile->getFile()->getSize() > 11485760) { // 10 MiB plus a bit of buffer
$context
->buildViolation('profiles.editor_page.additional_file_file_too_large')
->setTranslationDomain('messages')
->atPath('additionalFiles')
->addViolation();
}
}
}
}
/**
* @var DateTime
*
* @ORM\Column(name="paused_since", type="datetime", nullable=true)
*/
private $pausedSince;
public function getPausedSince(): ?DateTime
{
return $this->pausedSince;
}
public function setPausedSince(?DateTime $pausedSince = null): void
{
$this->pausedSince = $pausedSince;
}
public function isPaused(): bool
{
return $this->pausedSince !== null;
}
public function getStarsScoreRating(): float
{
$score = 2.5;
$step = 0.5;
$score += $this->getSelfdescription() ? $step : 0;
$score += $this->getMobilenumber() ? $step : 0;
$score += $this->getDocumentFileName() ? $step : 0;
$score += $this->getPhotoFileName() ? $step : 0;
$score += $this->hasAtLeastOneOccupationalFieldCapabilityAboveZero() ? $step : 0;
return $score;
}
public function getZipcodeIfAvailable(): ?string
{
if (!is_null($this->availabilityZipcode)) {
return $this->availabilityZipcode;
}
if (!is_null($this->zipcode)) {
return $this->zipcode;
}
return null;
}
}