TableTest.php 219 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\ORM;
  17. use ArrayObject;
  18. use AssertionError;
  19. use BadMethodCallException;
  20. use Cake\Collection\Collection;
  21. use Cake\Core\Exception\CakeException;
  22. use Cake\Database\Driver\Sqlserver;
  23. use Cake\Database\Exception\DatabaseException;
  24. use Cake\Database\Expression\IdentifierExpression;
  25. use Cake\Database\Expression\QueryExpression;
  26. use Cake\Database\Schema\TableSchema;
  27. use Cake\Database\StatementInterface;
  28. use Cake\Database\TypeMap;
  29. use Cake\Datasource\ConnectionManager;
  30. use Cake\Datasource\EntityInterface;
  31. use Cake\Datasource\Exception\InvalidPrimaryKeyException;
  32. use Cake\Datasource\Exception\RecordNotFoundException;
  33. use Cake\Datasource\ResultSetDecorator;
  34. use Cake\Event\EventInterface;
  35. use Cake\Event\EventManager;
  36. use Cake\I18n\DateTime;
  37. use Cake\ORM\Association\BelongsTo;
  38. use Cake\ORM\Association\BelongsToMany;
  39. use Cake\ORM\Association\HasMany;
  40. use Cake\ORM\Association\HasOne;
  41. use Cake\ORM\AssociationCollection;
  42. use Cake\ORM\Entity;
  43. use Cake\ORM\Exception\MissingBehaviorException;
  44. use Cake\ORM\Exception\MissingEntityException;
  45. use Cake\ORM\Exception\PersistenceFailedException;
  46. use Cake\ORM\Query\DeleteQuery;
  47. use Cake\ORM\Query\InsertQuery;
  48. use Cake\ORM\Query\SelectQuery;
  49. use Cake\ORM\Query\UpdateQuery;
  50. use Cake\ORM\RulesChecker;
  51. use Cake\ORM\Table;
  52. use Cake\TestSuite\TestCase;
  53. use Cake\Utility\Hash;
  54. use Cake\Validation\Validator;
  55. use Exception;
  56. use InvalidArgumentException;
  57. use PDOException;
  58. use PHPUnit\Framework\Attributes\WithoutErrorHandler;
  59. use RuntimeException;
  60. use TestApp\Model\Entity\ProtectedEntity;
  61. use TestApp\Model\Entity\Tag;
  62. use TestApp\Model\Entity\VirtualUser;
  63. use TestApp\Model\Table\ArticlesTable;
  64. use TestApp\Model\Table\UsersTable;
  65. /**
  66. * Tests Table class
  67. */
  68. class TableTest extends TestCase
  69. {
  70. /**
  71. * @var string[]
  72. */
  73. protected array $fixtures = [
  74. 'core.Articles',
  75. 'core.Tags',
  76. 'core.ArticlesTags',
  77. 'core.Authors',
  78. 'core.Categories',
  79. 'core.Comments',
  80. 'core.Sections',
  81. 'core.SectionsMembers',
  82. 'core.Members',
  83. 'core.PolymorphicTagged',
  84. 'core.SiteArticles',
  85. 'core.Users',
  86. ];
  87. /**
  88. * Handy variable containing the next primary key that will be inserted in the
  89. * users table
  90. *
  91. * @var int
  92. */
  93. protected static $nextUserId = 5;
  94. /**
  95. * @var \Cake\Datasource\ConnectionInterface
  96. */
  97. protected $connection;
  98. /**
  99. * @var \Cake\Database\TypeMap
  100. */
  101. protected $usersTypeMap;
  102. /**
  103. * @var \Cake\Database\TypeMap
  104. */
  105. protected $articlesTypeMap;
  106. public function setUp(): void
  107. {
  108. parent::setUp();
  109. $this->connection = ConnectionManager::get('test');
  110. static::setAppNamespace();
  111. $this->usersTypeMap = new TypeMap([
  112. 'Users.id' => 'integer',
  113. 'id' => 'integer',
  114. 'Users__id' => 'integer',
  115. 'Users.username' => 'string',
  116. 'Users__username' => 'string',
  117. 'username' => 'string',
  118. 'Users.password' => 'string',
  119. 'Users__password' => 'string',
  120. 'password' => 'string',
  121. 'Users.created' => 'timestamp',
  122. 'Users__created' => 'timestamp',
  123. 'created' => 'timestamp',
  124. 'Users.updated' => 'timestamp',
  125. 'Users__updated' => 'timestamp',
  126. 'updated' => 'timestamp',
  127. ]);
  128. $config = $this->connection->config();
  129. if (str_contains($config['driver'], 'Postgres')) {
  130. $this->usersTypeMap = new TypeMap([
  131. 'Users.id' => 'integer',
  132. 'id' => 'integer',
  133. 'Users__id' => 'integer',
  134. 'Users.username' => 'string',
  135. 'Users__username' => 'string',
  136. 'username' => 'string',
  137. 'Users.password' => 'string',
  138. 'Users__password' => 'string',
  139. 'password' => 'string',
  140. 'Users.created' => 'timestampfractional',
  141. 'Users__created' => 'timestampfractional',
  142. 'created' => 'timestampfractional',
  143. 'Users.updated' => 'timestampfractional',
  144. 'Users__updated' => 'timestampfractional',
  145. 'updated' => 'timestampfractional',
  146. ]);
  147. } elseif (str_contains($config['driver'], 'Sqlserver')) {
  148. $this->usersTypeMap = new TypeMap([
  149. 'Users.id' => 'integer',
  150. 'id' => 'integer',
  151. 'Users__id' => 'integer',
  152. 'Users.username' => 'string',
  153. 'Users__username' => 'string',
  154. 'username' => 'string',
  155. 'Users.password' => 'string',
  156. 'Users__password' => 'string',
  157. 'password' => 'string',
  158. 'Users.created' => 'datetimefractional',
  159. 'Users__created' => 'datetimefractional',
  160. 'created' => 'datetimefractional',
  161. 'Users.updated' => 'datetimefractional',
  162. 'Users__updated' => 'datetimefractional',
  163. 'updated' => 'datetimefractional',
  164. ]);
  165. }
  166. $this->articlesTypeMap = new TypeMap([
  167. 'Articles.id' => 'integer',
  168. 'Articles__id' => 'integer',
  169. 'id' => 'integer',
  170. 'Articles.title' => 'string',
  171. 'Articles__title' => 'string',
  172. 'title' => 'string',
  173. 'Articles.author_id' => 'integer',
  174. 'Articles__author_id' => 'integer',
  175. 'author_id' => 'integer',
  176. 'Articles.body' => 'text',
  177. 'Articles__body' => 'text',
  178. 'body' => 'text',
  179. 'Articles.published' => 'string',
  180. 'Articles__published' => 'string',
  181. 'published' => 'string',
  182. ]);
  183. }
  184. /**
  185. * teardown method
  186. */
  187. public function tearDown(): void
  188. {
  189. parent::tearDown();
  190. $this->clearPlugins();
  191. }
  192. /**
  193. * Tests query creation wrappers.
  194. */
  195. public function testTableQuery(): void
  196. {
  197. $table = new Table(['table' => 'users']);
  198. $query = $table->query();
  199. $this->assertEquals('users', $query->getRepository()->getTable());
  200. $query = $table->selectQuery();
  201. $this->assertEquals('users', $query->getRepository()->getTable());
  202. $query = $table->subquery();
  203. $this->assertEquals('users', $query->getRepository()->getTable());
  204. $sql = $query->select(['username'])->sql();
  205. $this->assertRegExpSql(
  206. 'SELECT <username> FROM <users> <users>',
  207. $sql,
  208. !$this->connection->getDriver()->isAutoQuotingEnabled()
  209. );
  210. }
  211. /**
  212. * Tests subquery() disables aliasing.
  213. */
  214. public function testSubqueryAliasing(): void
  215. {
  216. $articles = $this->getTableLocator()->get('Articles');
  217. $subquery = $articles->subquery();
  218. $subquery->select('Articles.field1');
  219. $this->assertRegExpSql(
  220. 'SELECT <Articles>.<field1> FROM <articles> <Articles>',
  221. $subquery->sql(),
  222. !$this->connection->getDriver()->isAutoQuotingEnabled()
  223. );
  224. $subquery->select($articles, true);
  225. $this->assertEqualsSql('SELECT id, author_id, title, body, published FROM articles Articles', $subquery->sql());
  226. $subquery->selectAllExcept($articles, ['author_id'], true);
  227. $this->assertEqualsSql('SELECT id, title, body, published FROM articles Articles', $subquery->sql());
  228. }
  229. /**
  230. * Tests subquery() in where clause.
  231. */
  232. public function testSubqueryWhereClause(): void
  233. {
  234. $subquery = $this->getTableLocator()->get('Authors')->subquery()
  235. ->select(['Authors.id'])
  236. ->where(['Authors.name' => 'mariano']);
  237. $query = $this->getTableLocator()->get('Articles')->find()
  238. ->where(['Articles.author_id IN' => $subquery])
  239. ->orderBy(['Articles.id' => 'ASC']);
  240. $results = $query->all()->toList();
  241. $this->assertCount(2, $results);
  242. $this->assertEquals([1, 3], array_column($results, 'id'));
  243. }
  244. /**
  245. * Tests subquery() in join clause.
  246. */
  247. public function testSubqueryJoinClause(): void
  248. {
  249. $subquery = $this->getTableLocator()->get('Articles')->subquery()
  250. ->select(['author_id']);
  251. $query = $this->getTableLocator()->get('Authors')->find();
  252. $query
  253. ->select(['Authors.id', 'total_articles' => $query->func()->count('articles.author_id')])
  254. ->leftJoin(['articles' => $subquery], ['articles.author_id' => new IdentifierExpression('Authors.id')])
  255. ->groupBy(['Authors.id'])
  256. ->orderBy(['Authors.id' => 'ASC']);
  257. $results = $query->all()->toList();
  258. $this->assertEquals(1, $results[0]->id);
  259. $this->assertEquals(2, $results[0]->total_articles);
  260. }
  261. /**
  262. * Tests the table method
  263. */
  264. public function testTableMethod(): void
  265. {
  266. $table = new Table(['table' => 'users']);
  267. $this->assertSame('users', $table->getTable());
  268. $table = new UsersTable();
  269. $this->assertSame('users', $table->getTable());
  270. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  271. $table = $this->getMockBuilder(Table::class)
  272. ->onlyMethods(['find'])
  273. ->setMockClassName('SpecialThingsTable')
  274. ->getMock();
  275. $this->assertSame('special_things', $table->getTable());
  276. $table = new Table(['alias' => 'LoveBoats']);
  277. $this->assertSame('love_boats', $table->getTable());
  278. $table->setTable('other');
  279. $this->assertSame('other', $table->getTable());
  280. $table->setTable('database.other');
  281. $this->assertSame('database.other', $table->getTable());
  282. }
  283. /**
  284. * Tests the setAlias method
  285. */
  286. public function testSetAlias(): void
  287. {
  288. $table = new Table(['alias' => 'users']);
  289. $this->assertSame('users', $table->getAlias());
  290. $table = new Table(['table' => 'stuffs']);
  291. $this->assertSame('stuffs', $table->getAlias());
  292. $table = new UsersTable();
  293. $this->assertSame('Users', $table->getAlias());
  294. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  295. $table = $this->getMockBuilder(Table::class)
  296. ->onlyMethods(['find'])
  297. ->setMockClassName('SpecialThingTable')
  298. ->getMock();
  299. $this->assertSame('SpecialThing', $table->getAlias());
  300. $table->setAlias('AnotherOne');
  301. $this->assertSame('AnotherOne', $table->getAlias());
  302. }
  303. public function testGetAliasException(): void
  304. {
  305. $this->expectException(CakeException::class);
  306. $this->expectExceptionMessage('You must specify either the `alias` or the `table` option for the constructor.');
  307. $table = new Table();
  308. $table->getAlias();
  309. }
  310. public function testGetTableException(): void
  311. {
  312. $this->expectException(CakeException::class);
  313. $this->expectExceptionMessage('You must specify either the `alias` or the `table` option for the constructor.');
  314. $table = new Table();
  315. $table->getTable();
  316. }
  317. /**
  318. * Test that aliasField() works.
  319. */
  320. public function testAliasField(): void
  321. {
  322. $table = new Table(['alias' => 'Users']);
  323. $this->assertSame('Users.id', $table->aliasField('id'));
  324. $this->assertSame('Users.id', $table->aliasField('Users.id'));
  325. }
  326. /**
  327. * Tests setConnection method
  328. */
  329. public function testSetConnection(): void
  330. {
  331. $table = new Table(['table' => 'users']);
  332. $this->assertSame($this->connection, $table->getConnection());
  333. $this->assertSame($table, $table->setConnection($this->connection));
  334. $this->assertSame($this->connection, $table->getConnection());
  335. }
  336. /**
  337. * Tests primaryKey method
  338. */
  339. public function testSetPrimaryKey(): void
  340. {
  341. $table = new Table([
  342. 'table' => 'users',
  343. 'schema' => [
  344. 'id' => ['type' => 'integer'],
  345. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  346. ],
  347. ]);
  348. $this->assertSame('id', $table->getPrimaryKey());
  349. $this->assertSame($table, $table->setPrimaryKey('thingID'));
  350. $this->assertSame('thingID', $table->getPrimaryKey());
  351. $table->setPrimaryKey(['thingID', 'user_id']);
  352. $this->assertEquals(['thingID', 'user_id'], $table->getPrimaryKey());
  353. }
  354. /**
  355. * Tests that name will be selected as a displayField
  356. */
  357. public function testDisplayFieldName(): void
  358. {
  359. $table = new Table([
  360. 'table' => 'users',
  361. 'schema' => [
  362. 'foo' => ['type' => 'string'],
  363. 'name' => ['type' => 'string'],
  364. ],
  365. ]);
  366. $this->assertSame('name', $table->getDisplayField());
  367. }
  368. /**
  369. * Tests that title will be selected as a displayField
  370. */
  371. public function testDisplayFieldTitle(): void
  372. {
  373. $table = new Table([
  374. 'table' => 'users',
  375. 'schema' => [
  376. 'foo' => ['type' => 'string'],
  377. 'title' => ['type' => 'string'],
  378. ],
  379. ]);
  380. $this->assertSame('title', $table->getDisplayField());
  381. }
  382. /**
  383. * Tests that label will be selected as a displayField
  384. */
  385. public function testDisplayFieldLabel(): void
  386. {
  387. $table = new Table([
  388. 'table' => 'users',
  389. 'schema' => [
  390. 'foo' => ['type' => 'string'],
  391. 'label' => ['type' => 'string'],
  392. ],
  393. ]);
  394. $this->assertSame('label', $table->getDisplayField());
  395. }
  396. /**
  397. * Tests that displayField will fallback to first *_name field
  398. */
  399. public function testDisplayNameFallback(): void
  400. {
  401. $table = new Table([
  402. 'table' => 'users',
  403. 'schema' => [
  404. 'id' => ['type' => 'integer'],
  405. 'custom_title' => ['type' => 'string'],
  406. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  407. ],
  408. ]);
  409. $this->assertSame('custom_title', $table->getDisplayField());
  410. $table = new Table([
  411. 'table' => 'users',
  412. 'schema' => [
  413. 'id' => ['type' => 'integer'],
  414. 'name' => ['type' => 'string'],
  415. 'custom_title' => ['type' => 'string'],
  416. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  417. ],
  418. ]);
  419. $this->assertSame('name', $table->getDisplayField());
  420. $table = new Table([
  421. 'table' => 'users',
  422. 'schema' => [
  423. 'id' => ['type' => 'integer'],
  424. 'title_id' => ['type' => 'integer'],
  425. 'custom_name' => ['type' => 'string'],
  426. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  427. ],
  428. ]);
  429. $this->assertSame('custom_name', $table->getDisplayField());
  430. $table = new Table([
  431. 'table' => 'users',
  432. 'schema' => [
  433. 'id' => ['type' => 'integer'],
  434. 'nullable_title' => ['type' => 'string', 'null' => true],
  435. 'custom_name' => ['type' => 'string'],
  436. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  437. ],
  438. ]);
  439. $this->assertSame('custom_name', $table->getDisplayField());
  440. $table = new Table([
  441. 'table' => 'users',
  442. 'schema' => [
  443. 'id' => ['type' => 'integer'],
  444. 'nullable_title' => ['type' => 'string', 'null' => true],
  445. 'password' => ['type' => 'string'],
  446. 'user_secret' => ['type' => 'string'],
  447. 'api_token' => ['type' => 'string'],
  448. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  449. ],
  450. ]);
  451. $this->assertSame('id', $table->getDisplayField());
  452. }
  453. /**
  454. * Tests that no displayField will fallback to primary key
  455. */
  456. public function testDisplayIdFallback(): void
  457. {
  458. $table = new Table([
  459. 'table' => 'users',
  460. 'schema' => [
  461. 'id' => ['type' => 'string'],
  462. 'foo' => ['type' => 'string'],
  463. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  464. ],
  465. ]);
  466. $this->assertSame('id', $table->getDisplayField());
  467. $table = $this->getTableLocator()->get('ArticlesTags');
  468. $this->assertSame(['article_id', 'tag_id'], $table->getDisplayField());
  469. }
  470. /**
  471. * Tests that displayField can be changed
  472. */
  473. public function testDisplaySet(): void
  474. {
  475. $table = new Table([
  476. 'table' => 'users',
  477. 'schema' => [
  478. 'id' => ['type' => 'string'],
  479. 'foo' => ['type' => 'string'],
  480. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  481. ],
  482. ]);
  483. $this->assertSame('id', $table->getDisplayField());
  484. $table->setDisplayField('foo');
  485. $this->assertSame('foo', $table->getDisplayField());
  486. }
  487. /**
  488. * Tests schema method
  489. */
  490. public function testSetSchema(): void
  491. {
  492. $schema = $this->connection->getSchemaCollection()->describe('users');
  493. $table = new Table([
  494. 'table' => 'users',
  495. 'connection' => $this->connection,
  496. ]);
  497. $this->assertEquals($schema, $table->getSchema());
  498. $table = new Table(['table' => 'stuff']);
  499. $table->setSchema($schema);
  500. $this->assertSame($schema, $table->getSchema());
  501. $table = new Table(['table' => 'another']);
  502. $schema = ['id' => ['type' => 'integer']];
  503. $table->setSchema($schema);
  504. $this->assertEquals(
  505. new TableSchema('another', $schema),
  506. $table->getSchema()
  507. );
  508. }
  509. /**
  510. * Tests schema method with long identifiers
  511. */
  512. public function testSetSchemaLongIdentifiers(): void
  513. {
  514. $schema = new TableSchema('long_identifiers', [
  515. 'this_is_invalid_because_it_is_very_very_very_long' => [
  516. 'type' => 'string',
  517. ],
  518. ]);
  519. $table = new Table([
  520. 'table' => 'very_long_alias_name',
  521. 'connection' => $this->connection,
  522. ]);
  523. $maxAlias = $this->connection->getDriver()->getMaxAliasLength();
  524. if ($maxAlias && $maxAlias < 72) {
  525. $nameLength = $maxAlias - 2;
  526. $this->expectException(DatabaseException::class);
  527. $this->expectExceptionMessage(
  528. 'ORM queries generate field aliases using the table name/alias and column name. ' .
  529. "The table alias `very_long_alias_name` and column `this_is_invalid_because_it_is_very_very_very_long` create an alias longer than ({$nameLength}). " .
  530. 'You must change the table schema in the database and shorten either the table or column ' .
  531. 'identifier so they fit within the database alias limits.'
  532. );
  533. }
  534. $this->assertNotNull($table->setSchema($schema));
  535. }
  536. public function testSchemaTypeOverrideInInitialize(): void
  537. {
  538. $table = new class (['alias' => 'Users', 'table' => 'users', 'connection' => $this->connection]) extends Table {
  539. public function initialize(array $config): void
  540. {
  541. $this->getSchema()->setColumnType('username', 'foobar');
  542. }
  543. };
  544. $result = $table->getSchema();
  545. $this->assertSame('foobar', $result->getColumnType('username'));
  546. }
  547. /**
  548. * Undocumented function
  549. *
  550. * @return void
  551. * @deprecated
  552. */
  553. #[WithoutErrorHandler]
  554. public function testFindAllOldStyleOptionsArray(): void
  555. {
  556. $this->deprecated(function () {
  557. $table = new Table([
  558. 'table' => 'users',
  559. 'connection' => $this->connection,
  560. ]);
  561. $query = $table->find('all', ['fields' => ['id']]);
  562. $this->assertSame(['id'], $query->clause('select'));
  563. });
  564. }
  565. /**
  566. * Tests that all fields for a table are added by default in a find when no
  567. * other fields are specified
  568. */
  569. public function testFindAllNoFieldsAndNoHydration(): void
  570. {
  571. $table = new Table([
  572. 'table' => 'users',
  573. 'connection' => $this->connection,
  574. ]);
  575. $results = $table
  576. ->find('all')
  577. ->where(['id IN' => [1, 2]])
  578. ->orderBy('id')
  579. ->enableHydration(false)
  580. ->toArray();
  581. $expected = [
  582. [
  583. 'id' => 1,
  584. 'username' => 'mariano',
  585. 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO',
  586. 'created' => new DateTime('2007-03-17 01:16:23'),
  587. 'updated' => new DateTime('2007-03-17 01:18:31'),
  588. ],
  589. [
  590. 'id' => 2,
  591. 'username' => 'nate',
  592. 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO',
  593. 'created' => new DateTime('2008-03-17 01:18:23'),
  594. 'updated' => new DateTime('2008-03-17 01:20:31'),
  595. ],
  596. ];
  597. $this->assertEquals($expected, $results);
  598. }
  599. /**
  600. * Tests that it is possible to select only a few fields when finding over a table
  601. */
  602. public function testFindAllSomeFieldsNoHydration(): void
  603. {
  604. $table = new Table([
  605. 'table' => 'users',
  606. 'connection' => $this->connection,
  607. ]);
  608. $results = $table->find('all')
  609. ->select(['username', 'password'])
  610. ->enableHydration(false)
  611. ->orderBy('username')->toArray();
  612. $expected = [
  613. ['username' => 'garrett', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  614. ['username' => 'larry', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  615. ['username' => 'mariano', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  616. ['username' => 'nate', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  617. ];
  618. $this->assertSame($expected, $results);
  619. $results = $table->find('all')
  620. ->select(['foo' => 'username', 'password'])
  621. ->orderBy('username')
  622. ->enableHydration(false)
  623. ->toArray();
  624. $expected = [
  625. ['foo' => 'garrett', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  626. ['foo' => 'larry', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  627. ['foo' => 'mariano', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  628. ['foo' => 'nate', 'password' => '$2a$10$u05j8FjsvLBNdfhBhc21LOuVMpzpabVXQ9OpC2wO3pSO0q6t7HHMO'],
  629. ];
  630. $this->assertSame($expected, $results);
  631. }
  632. /**
  633. * Tests that the query will automatically casts complex conditions to the correct
  634. * types when the columns belong to the default table
  635. */
  636. public function testFindAllConditionAutoTypes(): void
  637. {
  638. $table = new Table([
  639. 'table' => 'users',
  640. 'connection' => $this->connection,
  641. ]);
  642. $query = $table->find('all')
  643. ->select(['id', 'username'])
  644. ->where(['created >=' => new DateTime('2010-01-22 00:00')])
  645. ->enableHydration(false)
  646. ->orderBy('id');
  647. $expected = [
  648. ['id' => 3, 'username' => 'larry'],
  649. ['id' => 4, 'username' => 'garrett'],
  650. ];
  651. $this->assertSame($expected, $query->toArray());
  652. $query = $table->find()
  653. ->enableHydration(false)
  654. ->select(['id', 'username'])
  655. ->where(['OR' => [
  656. 'created >=' => new DateTime('2010-01-22 00:00'),
  657. 'users.created' => new DateTime('2008-03-17 01:18:23'),
  658. ]])
  659. ->orderBy('id');
  660. $expected = [
  661. ['id' => 2, 'username' => 'nate'],
  662. ['id' => 3, 'username' => 'larry'],
  663. ['id' => 4, 'username' => 'garrett'],
  664. ];
  665. $this->assertSame($expected, $query->toArray());
  666. }
  667. /**
  668. * Test that beforeFind events can mutate the query.
  669. */
  670. public function testFindBeforeFindEventMutateQuery(): void
  671. {
  672. $table = new Table([
  673. 'table' => 'users',
  674. 'connection' => $this->connection,
  675. ]);
  676. $table->getEventManager()->on(
  677. 'Model.beforeFind',
  678. function (EventInterface $event, $query, $options): void {
  679. $query->limit(1);
  680. }
  681. );
  682. $result = $table->find('all')->all();
  683. $this->assertCount(1, $result, 'Should only have 1 record, limit 1 applied.');
  684. }
  685. /**
  686. * Test that beforeFind events are fired and can stop the find and
  687. * return custom results.
  688. */
  689. public function testFindBeforeFindEventOverrideReturn(): void
  690. {
  691. $table = new Table([
  692. 'table' => 'users',
  693. 'connection' => $this->connection,
  694. ]);
  695. $expected = ['One', 'Two', 'Three'];
  696. $table->getEventManager()->on(
  697. 'Model.beforeFind',
  698. function (EventInterface $event, $query, $options) use ($expected): void {
  699. $query->setResult($expected);
  700. $event->stopPropagation();
  701. }
  702. );
  703. $query = $table->find('all')
  704. ->formatResults(function (ResultSetDecorator $results) {
  705. return $results;
  706. });
  707. $query->limit(1);
  708. $this->assertEquals($expected, $query->all()->toArray());
  709. }
  710. /**
  711. * Test that the getAssociation() method supports the dot syntax.
  712. */
  713. public function testAssociationDotSyntax(): void
  714. {
  715. $sections = $this->getTableLocator()->get('Sections');
  716. $members = $this->getTableLocator()->get('Members');
  717. $sectionsMembers = $this->getTableLocator()->get('SectionsMembers');
  718. $sections->belongsToMany('Members');
  719. $sections->hasMany('SectionsMembers');
  720. $sectionsMembers->belongsTo('Members');
  721. $members->belongsToMany('Sections');
  722. $association = $sections->getAssociation('SectionsMembers.Members.Sections');
  723. $this->assertInstanceOf(BelongsToMany::class, $association);
  724. $this->assertSame(
  725. $sections->getAssociation('SectionsMembers')->getAssociation('Members')->getAssociation('Sections'),
  726. $association
  727. );
  728. }
  729. public function testGetAssociationWithIncorrectCasing(): void
  730. {
  731. $this->expectException(InvalidArgumentException::class);
  732. $this->expectExceptionMessage(
  733. "The `authors` association is not defined on `Articles`.\n"
  734. . 'Valid associations are: Authors, Tags, ArticlesTags'
  735. );
  736. $articles = $this->getTableLocator()->get('Articles', ['className' => ArticlesTable::class]);
  737. $articles->getAssociation('authors');
  738. }
  739. /**
  740. * Tests that the getAssociation() method throws an exception on nonexistent ones.
  741. */
  742. public function testGetAssociationNonExistent(): void
  743. {
  744. $this->expectException(InvalidArgumentException::class);
  745. $this->expectExceptionMessage('The `FooBar` association is not defined on `Sections`.');
  746. $this->getTableLocator()->get('Sections')->getAssociation('FooBar');
  747. }
  748. /**
  749. * Tests that belongsTo() creates and configures correctly the association
  750. */
  751. public function testBelongsTo(): void
  752. {
  753. $options = ['foreignKey' => 'fake_id', 'conditions' => ['a' => 'b']];
  754. $table = new Table(['table' => 'dates']);
  755. $belongsTo = $table->belongsTo('user', $options);
  756. $this->assertInstanceOf(BelongsTo::class, $belongsTo);
  757. $this->assertSame($belongsTo, $table->getAssociation('user'));
  758. $this->assertSame('user', $belongsTo->getName());
  759. $this->assertSame('fake_id', $belongsTo->getForeignKey());
  760. $this->assertEquals(['a' => 'b'], $belongsTo->getConditions());
  761. $this->assertSame($table, $belongsTo->getSource());
  762. }
  763. /**
  764. * Tests that hasOne() creates and configures correctly the association
  765. */
  766. public function testHasOne(): void
  767. {
  768. $table = new Table(['table' => 'users']);
  769. $hasOne = $table->hasOne('profile', ['conditions' => ['b' => 'c']]);
  770. $this->assertInstanceOf(HasOne::class, $hasOne);
  771. $this->assertSame($hasOne, $table->getAssociation('profile'));
  772. $this->assertSame('profile', $hasOne->getName());
  773. $this->assertSame('user_id', $hasOne->getForeignKey());
  774. $this->assertEquals(['b' => 'c'], $hasOne->getConditions());
  775. $this->assertSame($table, $hasOne->getSource());
  776. }
  777. /**
  778. * Test has one with a plugin model
  779. */
  780. public function testHasOnePlugin(): void
  781. {
  782. $table = new Table(['table' => 'users']);
  783. $hasOne = $table->hasOne('Comments', ['className' => 'TestPlugin.Comments']);
  784. $this->assertInstanceOf(HasOne::class, $hasOne);
  785. $this->assertSame('Comments', $hasOne->getName());
  786. $this->assertSame('Comments', $hasOne->getAlias());
  787. $this->assertSame('TestPlugin.Comments', $hasOne->getRegistryAlias());
  788. $table = new Table(['table' => 'users']);
  789. $hasOne = $table->hasOne('TestPlugin.Comments', ['className' => 'TestPlugin.Comments']);
  790. $this->assertInstanceOf(HasOne::class, $hasOne);
  791. $this->assertSame('Comments', $hasOne->getName());
  792. $this->assertSame('Comments', $hasOne->getAlias());
  793. $this->assertSame('TestPlugin.Comments', $hasOne->getRegistryAlias());
  794. }
  795. /**
  796. * testNoneUniqueAssociationsSameClass
  797. */
  798. public function testNoneUniqueAssociationsSameClass(): void
  799. {
  800. $Users = new Table(['table' => 'users']);
  801. $Users->hasMany('Comments');
  802. $Articles = new Table(['table' => 'articles']);
  803. $Articles->hasMany('Comments');
  804. $Categories = new Table(['table' => 'categories']);
  805. $options = ['className' => 'TestPlugin.Comments'];
  806. $Categories->hasMany('Comments', $options);
  807. $this->assertInstanceOf(Table::class, $Users->Comments->getTarget());
  808. $this->assertInstanceOf(Table::class, $Articles->Comments->getTarget());
  809. $this->assertInstanceOf('TestPlugin\Model\Table\CommentsTable', $Categories->Comments->getTarget());
  810. }
  811. /**
  812. * Test associations which refer to the same table multiple times
  813. */
  814. public function testSelfJoinAssociations(): void
  815. {
  816. $Categories = $this->getTableLocator()->get('Categories');
  817. $options = ['className' => 'Categories'];
  818. $Categories->hasMany('Children', ['foreignKey' => 'parent_id'] + $options);
  819. $Categories->belongsTo('Parent', $options);
  820. $this->assertSame('categories', $Categories->Children->getTarget()->getTable());
  821. $this->assertSame('categories', $Categories->Parent->getTarget()->getTable());
  822. $this->assertSame('Children', $Categories->Children->getAlias());
  823. $this->assertSame('Children', $Categories->Children->getTarget()->getAlias());
  824. $this->assertSame('Parent', $Categories->Parent->getAlias());
  825. $this->assertSame('Parent', $Categories->Parent->getTarget()->getAlias());
  826. $expected = [
  827. 'id' => 2,
  828. 'parent_id' => 1,
  829. 'name' => 'Category 1.1',
  830. 'parent' => [
  831. 'id' => 1,
  832. 'parent_id' => 0,
  833. 'name' => 'Category 1',
  834. ],
  835. 'children' => [
  836. [
  837. 'id' => 7,
  838. 'parent_id' => 2,
  839. 'name' => 'Category 1.1.1',
  840. ],
  841. [
  842. 'id' => 8,
  843. 'parent_id' => 2,
  844. 'name' => 'Category 1.1.2',
  845. ],
  846. ],
  847. ];
  848. $fields = ['id', 'parent_id', 'name'];
  849. $result = $Categories->find('all')
  850. ->select(['Categories.id', 'Categories.parent_id', 'Categories.name'])
  851. ->contain(['Children' => ['fields' => $fields], 'Parent' => ['fields' => $fields]])
  852. ->where(['Categories.id' => 2])
  853. ->first()
  854. ->toArray();
  855. $this->assertSame($expected, $result);
  856. }
  857. /**
  858. * Tests that hasMany() creates and configures correctly the association
  859. */
  860. public function testHasMany(): void
  861. {
  862. $options = [
  863. 'conditions' => ['b' => 'c'],
  864. 'sort' => ['foo' => 'asc'],
  865. ];
  866. $table = new Table(['table' => 'authors']);
  867. $hasMany = $table->hasMany('article', $options);
  868. $this->assertInstanceOf(HasMany::class, $hasMany);
  869. $this->assertSame($hasMany, $table->getAssociation('article'));
  870. $this->assertSame('article', $hasMany->getName());
  871. $this->assertSame('author_id', $hasMany->getForeignKey());
  872. $this->assertEquals(['b' => 'c'], $hasMany->getConditions());
  873. $this->assertEquals(['foo' => 'asc'], $hasMany->getSort());
  874. $this->assertSame($table, $hasMany->getSource());
  875. }
  876. /**
  877. * testHasManyWithClassName
  878. */
  879. public function testHasManyWithClassName(): void
  880. {
  881. $table = $this->getTableLocator()->get('Articles');
  882. $table->hasMany('Comments', [
  883. 'conditions' => ['published' => 'Y'],
  884. ]);
  885. $table->hasMany('UnapprovedComments', [
  886. 'className' => 'Comments',
  887. 'conditions' => ['published' => 'N'],
  888. 'propertyName' => 'unaproved_comments',
  889. ]);
  890. $expected = [
  891. 'id' => 1,
  892. 'title' => 'First Article',
  893. 'unaproved_comments' => [
  894. [
  895. 'id' => 4,
  896. 'article_id' => 1,
  897. 'comment' => 'Fourth Comment for First Article',
  898. ],
  899. ],
  900. 'comments' => [
  901. [
  902. 'id' => 1,
  903. 'article_id' => 1,
  904. 'comment' => 'First Comment for First Article',
  905. ],
  906. [
  907. 'id' => 2,
  908. 'article_id' => 1,
  909. 'comment' => 'Second Comment for First Article',
  910. ],
  911. [
  912. 'id' => 3,
  913. 'article_id' => 1,
  914. 'comment' => 'Third Comment for First Article',
  915. ],
  916. ],
  917. ];
  918. $result = $table->find()
  919. ->select(['id', 'title'])
  920. ->contain([
  921. 'Comments' => ['fields' => ['id', 'article_id', 'comment']],
  922. 'UnapprovedComments' => ['fields' => ['id', 'article_id', 'comment']],
  923. ])
  924. ->where(['id' => 1])
  925. ->first();
  926. $this->assertSame($expected, $result->toArray());
  927. }
  928. /**
  929. * Ensure associations use the plugin-prefixed model
  930. */
  931. public function testHasManyPluginOverlap(): void
  932. {
  933. $this->getTableLocator()->get('Comments');
  934. $this->loadPlugins(['TestPlugin']);
  935. $table = new Table(['table' => 'authors']);
  936. $table->hasMany('TestPlugin.Comments');
  937. $comments = $table->Comments->getTarget();
  938. $this->assertInstanceOf('TestPlugin\Model\Table\CommentsTable', $comments);
  939. }
  940. /**
  941. * Ensure associations use the plugin-prefixed model
  942. * even if specified with config
  943. */
  944. public function testHasManyPluginOverlapConfig(): void
  945. {
  946. $this->getTableLocator()->get('Comments');
  947. $this->loadPlugins(['TestPlugin']);
  948. $table = new Table(['table' => 'authors']);
  949. $table->hasMany('Comments', ['className' => 'TestPlugin.Comments']);
  950. $comments = $table->Comments->getTarget();
  951. $this->assertInstanceOf('TestPlugin\Model\Table\CommentsTable', $comments);
  952. }
  953. /**
  954. * Tests that BelongsToMany() creates and configures correctly the association
  955. */
  956. public function testBelongsToMany(): void
  957. {
  958. $options = [
  959. 'foreignKey' => 'thing_id',
  960. 'joinTable' => 'things_tags',
  961. 'conditions' => ['b' => 'c'],
  962. 'sort' => ['foo' => 'asc'],
  963. ];
  964. $table = new Table(['table' => 'authors', 'connection' => $this->connection]);
  965. $belongsToMany = $table->belongsToMany('tag', $options);
  966. $this->assertInstanceOf(BelongsToMany::class, $belongsToMany);
  967. $this->assertSame($belongsToMany, $table->getAssociation('tag'));
  968. $this->assertSame('tag', $belongsToMany->getName());
  969. $this->assertSame('thing_id', $belongsToMany->getForeignKey());
  970. $this->assertEquals(['b' => 'c'], $belongsToMany->getConditions());
  971. $this->assertEquals(['foo' => 'asc'], $belongsToMany->getSort());
  972. $this->assertSame($table, $belongsToMany->getSource());
  973. $this->assertSame('things_tags', $belongsToMany->junction()->getTable());
  974. }
  975. /**
  976. * Test addAssociations()
  977. */
  978. public function testAddAssociations(): void
  979. {
  980. $params = [
  981. 'belongsTo' => [
  982. 'users' => ['foreignKey' => 'fake_id', 'conditions' => ['a' => 'b']],
  983. ],
  984. 'hasOne' => ['profiles'],
  985. 'hasMany' => ['authors'],
  986. 'belongsToMany' => [
  987. 'tags' => [
  988. 'joinTable' => 'things_tags',
  989. 'conditions' => [
  990. 'Tags.starred' => true,
  991. ],
  992. ],
  993. ],
  994. ];
  995. $table = new Table(['table' => 'members']);
  996. $result = $table->addAssociations($params);
  997. $this->assertSame($table, $result);
  998. $associations = $table->associations();
  999. $belongsTo = $associations->get('users');
  1000. $this->assertInstanceOf('Cake\ORM\Association\BelongsTo', $belongsTo);
  1001. $this->assertSame('users', $belongsTo->getName());
  1002. $this->assertSame('fake_id', $belongsTo->getForeignKey());
  1003. $this->assertEquals(['a' => 'b'], $belongsTo->getConditions());
  1004. $this->assertSame($table, $belongsTo->getSource());
  1005. $hasOne = $associations->get('profiles');
  1006. $this->assertInstanceOf(HasOne::class, $hasOne);
  1007. $this->assertSame('profiles', $hasOne->getName());
  1008. $hasMany = $associations->get('authors');
  1009. $this->assertInstanceOf(HasMany::class, $hasMany);
  1010. $this->assertSame('authors', $hasMany->getName());
  1011. $belongsToMany = $associations->get('tags');
  1012. $this->assertInstanceOf(BelongsToMany::class, $belongsToMany);
  1013. $this->assertSame('tags', $belongsToMany->getName());
  1014. $this->assertSame('things_tags', $belongsToMany->junction()->getTable());
  1015. $this->assertSame(['Tags.starred' => true], $belongsToMany->getConditions());
  1016. }
  1017. /**
  1018. * Test basic multi row updates.
  1019. */
  1020. public function testUpdateAll(): void
  1021. {
  1022. $table = new Table([
  1023. 'table' => 'users',
  1024. 'connection' => $this->connection,
  1025. ]);
  1026. $fields = ['username' => 'mark'];
  1027. $result = $table->updateAll($fields, ['id <' => 4]);
  1028. $this->assertSame(3, $result);
  1029. $result = $table->find('all')
  1030. ->select(['username'])
  1031. ->orderBy(['id' => 'asc'])
  1032. ->enableHydration(false)
  1033. ->toArray();
  1034. $expected = array_fill(0, 3, $fields);
  1035. $expected[] = ['username' => 'garrett'];
  1036. $this->assertEquals($expected, $result);
  1037. }
  1038. /**
  1039. * Test that exceptions from the Query bubble up.
  1040. */
  1041. public function testUpdateAllFailure(): void
  1042. {
  1043. $this->expectException(DatabaseException::class);
  1044. $table = $this->getMockBuilder(Table::class)
  1045. ->onlyMethods(['updateQuery'])
  1046. ->setConstructorArgs([['table' => 'users']])
  1047. ->getMock();
  1048. $query = $this->getMockBuilder(UpdateQuery::class)
  1049. ->onlyMethods(['execute'])
  1050. ->setConstructorArgs([$table])
  1051. ->getMock();
  1052. $table->expects($this->once())
  1053. ->method('updateQuery')
  1054. ->willReturn($query);
  1055. $query->expects($this->once())
  1056. ->method('execute')
  1057. ->will($this->throwException(new DatabaseException('Not good')));
  1058. $table->updateAll(['username' => 'mark'], []);
  1059. }
  1060. /**
  1061. * Test deleting many records.
  1062. */
  1063. public function testDeleteAll(): void
  1064. {
  1065. $table = new Table([
  1066. 'table' => 'users',
  1067. 'connection' => $this->connection,
  1068. ]);
  1069. $result = $table->deleteAll(['id <' => 4]);
  1070. $this->assertSame(3, $result);
  1071. $result = $table->find('all')->toArray();
  1072. $this->assertCount(1, $result, 'Only one record should remain');
  1073. $this->assertSame(4, $result[0]['id']);
  1074. }
  1075. /**
  1076. * Test deleting many records with conditions using the alias
  1077. */
  1078. public function testDeleteAllAliasedConditions(): void
  1079. {
  1080. $table = new Table([
  1081. 'table' => 'users',
  1082. 'alias' => 'Managers',
  1083. 'connection' => $this->connection,
  1084. ]);
  1085. $result = $table->deleteAll(['Managers.id <' => 4]);
  1086. $this->assertSame(3, $result);
  1087. $result = $table->find('all')->toArray();
  1088. $this->assertCount(1, $result, 'Only one record should remain');
  1089. $this->assertSame(4, $result[0]['id']);
  1090. }
  1091. /**
  1092. * Test that exceptions from the Query bubble up.
  1093. */
  1094. public function testDeleteAllFailure(): void
  1095. {
  1096. $this->expectException(DatabaseException::class);
  1097. $table = $this->getMockBuilder(Table::class)
  1098. ->onlyMethods(['deleteQuery'])
  1099. ->setConstructorArgs([['table' => 'users']])
  1100. ->getMock();
  1101. $query = $this->getMockBuilder(DeleteQuery::class)
  1102. ->onlyMethods(['execute'])
  1103. ->setConstructorArgs([$table])
  1104. ->getMock();
  1105. $table->expects($this->once())
  1106. ->method('deleteQuery')
  1107. ->willReturn($query);
  1108. $query->expects($this->once())
  1109. ->method('execute')
  1110. ->will($this->throwException(new DatabaseException('Not good')));
  1111. $table->deleteAll(['id >' => 4]);
  1112. }
  1113. /**
  1114. * Tests that array options are passed to the query object using applyOptions
  1115. */
  1116. public function testFindApplyOptions(): void
  1117. {
  1118. $table = $this->getMockBuilder(Table::class)
  1119. ->onlyMethods(['selectQuery', 'findAll'])
  1120. ->setConstructorArgs([['table' => 'users', 'connection' => $this->connection]])
  1121. ->getMock();
  1122. $query = $this->getMockBuilder(SelectQuery::class)
  1123. ->setConstructorArgs([$table])
  1124. ->getMock();
  1125. $table->expects($this->once())
  1126. ->method('selectQuery')
  1127. ->willReturn($query);
  1128. $options = ['fields' => ['a', 'b']];
  1129. $query->expects($this->any())
  1130. ->method('select')
  1131. ->willReturnSelf();
  1132. $query->expects($this->once())->method('getOptions')
  1133. ->willReturn([]);
  1134. $query->expects($this->once())
  1135. ->method('applyOptions')
  1136. ->with($options);
  1137. $table->expects($this->once())->method('findAll');
  1138. $table->find('all', ...$options);
  1139. }
  1140. /**
  1141. * Tests that extra arguments are passed to finders.
  1142. */
  1143. public function testFindTypedParameters(): void
  1144. {
  1145. $author = $this->getTableLocator()->get('Authors')->find('WithIdArgument', 2)->first();
  1146. $this->assertSame(2, $author->id);
  1147. $author = $this->getTableLocator()->get('Authors')->find('WithIdArgument', id: 2)->first();
  1148. $this->assertSame(2, $author->id);
  1149. }
  1150. public function testFindTypedParameterCompatibility(): void
  1151. {
  1152. $articles = $this->fetchTable('Articles');
  1153. $article = $articles->find('titled')->first();
  1154. $this->assertNotEmpty($article);
  1155. // Options arrays are deprecated but should work
  1156. $this->deprecated(function () use ($articles) {
  1157. $article = $articles->find('titled', ['title' => 'Second Article'])->first();
  1158. $this->assertNotEmpty($article);
  1159. $this->assertEquals('Second Article', $article->title);
  1160. });
  1161. // Named parameters should be compatible with options finders
  1162. $article = $articles->find('titled', title: 'Second Article')->first();
  1163. $this->assertNotEmpty($article);
  1164. $this->assertEquals('Second Article', $article->title);
  1165. }
  1166. public function testFindForFinderVariadic(): void
  1167. {
  1168. $testTable = $this->fetchTable('Test');
  1169. $testTable->find('variadic', foo: 'bar');
  1170. $this->assertNull($testTable->first);
  1171. $this->assertSame(['foo' => 'bar'], $testTable->variadic);
  1172. $testTable->find('variadic', first: 'one', foo: 'bar');
  1173. $this->assertSame('one', $testTable->first);
  1174. $this->assertSame(['foo' => 'bar'], $testTable->variadic);
  1175. $testTable->find('variadicOptions');
  1176. $this->assertSame([], $testTable->variadicOptions);
  1177. $testTable->find('variadicOptions', foo: 'bar');
  1178. $this->assertSame(['foo' => 'bar'], $testTable->variadicOptions);
  1179. }
  1180. /**
  1181. * Tests find('list')
  1182. */
  1183. public function testFindListNoHydration(): void
  1184. {
  1185. $table = new Table([
  1186. 'table' => 'users',
  1187. 'connection' => $this->connection,
  1188. ]);
  1189. $table->setDisplayField('username');
  1190. $query = $table->find('list')
  1191. ->enableHydration(false)
  1192. ->orderBy('id');
  1193. $expected = [
  1194. 1 => 'mariano',
  1195. 2 => 'nate',
  1196. 3 => 'larry',
  1197. 4 => 'garrett',
  1198. ];
  1199. $this->assertSame($expected, $query->toArray());
  1200. $query = $table->find('list', fields: ['id', 'username'])
  1201. ->enableHydration(false)
  1202. ->orderBy('id');
  1203. $expected = [
  1204. 1 => 'mariano',
  1205. 2 => 'nate',
  1206. 3 => 'larry',
  1207. 4 => 'garrett',
  1208. ];
  1209. $this->assertSame($expected, $query->toArray());
  1210. $query = $table->find('list', groupField: 'odd')
  1211. ->select(['id', 'username', 'odd' => new QueryExpression('id % 2')])
  1212. ->enableHydration(false)
  1213. ->orderBy('id');
  1214. $expected = [
  1215. 1 => [
  1216. 1 => 'mariano',
  1217. 3 => 'larry',
  1218. ],
  1219. 0 => [
  1220. 2 => 'nate',
  1221. 4 => 'garrett',
  1222. ],
  1223. ];
  1224. $this->assertSame($expected, $query->toArray());
  1225. }
  1226. /**
  1227. * Tests find('threaded')
  1228. */
  1229. public function testFindThreadedNoHydration(): void
  1230. {
  1231. $table = new Table([
  1232. 'table' => 'categories',
  1233. 'connection' => $this->connection,
  1234. ]);
  1235. $expected = [
  1236. [
  1237. 'id' => 1,
  1238. 'parent_id' => 0,
  1239. 'name' => 'Category 1',
  1240. 'children' => [
  1241. [
  1242. 'id' => 2,
  1243. 'parent_id' => 1,
  1244. 'name' => 'Category 1.1',
  1245. 'children' => [
  1246. [
  1247. 'id' => 7,
  1248. 'parent_id' => 2,
  1249. 'name' => 'Category 1.1.1',
  1250. 'children' => [],
  1251. ],
  1252. [
  1253. 'id' => 8,
  1254. 'parent_id' => '2',
  1255. 'name' => 'Category 1.1.2',
  1256. 'children' => [],
  1257. ],
  1258. ],
  1259. ],
  1260. [
  1261. 'id' => 3,
  1262. 'parent_id' => '1',
  1263. 'name' => 'Category 1.2',
  1264. 'children' => [],
  1265. ],
  1266. ],
  1267. ],
  1268. [
  1269. 'id' => 4,
  1270. 'parent_id' => 0,
  1271. 'name' => 'Category 2',
  1272. 'children' => [],
  1273. ],
  1274. [
  1275. 'id' => 5,
  1276. 'parent_id' => 0,
  1277. 'name' => 'Category 3',
  1278. 'children' => [
  1279. [
  1280. 'id' => '6',
  1281. 'parent_id' => '5',
  1282. 'name' => 'Category 3.1',
  1283. 'children' => [],
  1284. ],
  1285. ],
  1286. ],
  1287. ];
  1288. $results = $table->find('all')
  1289. ->select(['id', 'parent_id', 'name'])
  1290. ->enableHydration(false)
  1291. ->find('threaded')
  1292. ->toArray();
  1293. $this->assertEquals($expected, $results);
  1294. }
  1295. /**
  1296. * Tests that finders can be stacked
  1297. */
  1298. public function testStackingFinders(): void
  1299. {
  1300. $table = $this->getMockBuilder(Table::class)
  1301. ->onlyMethods(['find', 'findList'])
  1302. ->disableOriginalConstructor()
  1303. ->getMock();
  1304. $query = $this->getMockBuilder('Cake\ORM\Query')
  1305. ->onlyMethods(['addDefaultTypes'])
  1306. ->setConstructorArgs([$table])
  1307. ->getMock();
  1308. $table->expects($this->once())
  1309. ->method('find')
  1310. ->with('threaded', ['order' => ['name' => 'ASC']])
  1311. ->willReturn($query);
  1312. $table->expects($this->once())
  1313. ->method('findList')
  1314. ->with($query, 'id')
  1315. ->willReturn($query);
  1316. $result = $table
  1317. ->find('threaded', ['order' => ['name' => 'ASC']])
  1318. ->find('list', keyField: 'id');
  1319. $this->assertSame($query, $result);
  1320. }
  1321. /**
  1322. * Tests find('threaded') with hydrated results
  1323. */
  1324. public function testFindThreadedHydrated(): void
  1325. {
  1326. $table = new Table([
  1327. 'table' => 'categories',
  1328. 'connection' => $this->connection,
  1329. ]);
  1330. $results = $table->find('all')
  1331. ->find('threaded')
  1332. ->select(['id', 'parent_id', 'name'])
  1333. ->toArray();
  1334. $this->assertSame(1, $results[0]->id);
  1335. $expected = [
  1336. 'id' => 8,
  1337. 'parent_id' => 2,
  1338. 'name' => 'Category 1.1.2',
  1339. 'children' => [],
  1340. ];
  1341. $this->assertEquals($expected, $results[0]->children[0]->children[1]->toArray());
  1342. }
  1343. /**
  1344. * Tests find('list') with hydrated records
  1345. */
  1346. public function testFindListHydrated(): void
  1347. {
  1348. $table = new Table([
  1349. 'table' => 'users',
  1350. 'connection' => $this->connection,
  1351. ]);
  1352. $table->setDisplayField('username');
  1353. $query = $table
  1354. ->find('list', fields: ['id', 'username'])
  1355. ->orderBy('id');
  1356. $expected = [
  1357. 1 => 'mariano',
  1358. 2 => 'nate',
  1359. 3 => 'larry',
  1360. 4 => 'garrett',
  1361. ];
  1362. $this->assertSame($expected, $query->toArray());
  1363. $query = $table->find('list', groupField: 'odd')
  1364. ->select(['id', 'username', 'odd' => new QueryExpression('id % 2')])
  1365. ->enableHydration(true)
  1366. ->orderBy('id');
  1367. $expected = [
  1368. 1 => [
  1369. 1 => 'mariano',
  1370. 3 => 'larry',
  1371. ],
  1372. 0 => [
  1373. 2 => 'nate',
  1374. 4 => 'garrett',
  1375. ],
  1376. ];
  1377. $this->assertSame($expected, $query->toArray());
  1378. }
  1379. /**
  1380. * Test that find('list') only selects required fields.
  1381. */
  1382. public function testFindListSelectedFields(): void
  1383. {
  1384. $table = new Table([
  1385. 'table' => 'users',
  1386. 'connection' => $this->connection,
  1387. ]);
  1388. $table->setDisplayField('username');
  1389. $query = $table->find('list');
  1390. $expected = ['id', 'username'];
  1391. $this->assertSame($expected, $query->clause('select'));
  1392. $query = $table->find('list', valueField: function ($row) {
  1393. return $row->username;
  1394. });
  1395. $this->assertEmpty($query->clause('select'));
  1396. $expected = ['odd' => new QueryExpression('id % 2'), 'id', 'username'];
  1397. $query = $table->find('list', fields: $expected, groupField: 'odd');
  1398. $this->assertSame($expected, $query->clause('select'));
  1399. $articles = new Table([
  1400. 'table' => 'articles',
  1401. 'connection' => $this->connection,
  1402. ]);
  1403. $query = $articles->find('list', groupField: 'author_id');
  1404. $expected = ['id', 'title', 'author_id'];
  1405. $this->assertSame($expected, $query->clause('select'));
  1406. $query = $articles->find('list', valueField: ['author_id', 'title'])
  1407. ->orderBy('id');
  1408. $expected = ['id', 'author_id', 'title'];
  1409. $this->assertSame($expected, $query->clause('select'));
  1410. $expected = [
  1411. 1 => '1;First Article',
  1412. 2 => '3;Second Article',
  1413. 3 => '1;Third Article',
  1414. ];
  1415. $this->assertSame($expected, $query->toArray());
  1416. $query = $articles->find('list', valueField: ['id', 'title'], valueSeparator: ' : ')
  1417. ->orderBy('id');
  1418. $expected = [
  1419. 1 => '1 : First Article',
  1420. 2 => '2 : Second Article',
  1421. 3 => '3 : Third Article',
  1422. ];
  1423. $this->assertSame($expected, $query->toArray());
  1424. }
  1425. /**
  1426. * Tests find(list) with backwards compatibile options
  1427. */
  1428. #[WithoutErrorHandler]
  1429. public function testFindListArrayOptions(): void
  1430. {
  1431. $table = new Table([
  1432. 'table' => 'users',
  1433. 'connection' => $this->connection,
  1434. ]);
  1435. $table->setDisplayField('username');
  1436. $this->deprecated(function () use ($table) {
  1437. $query = $table
  1438. ->find('list', ['fields' => ['id', 'username']])
  1439. ->orderBy('id');
  1440. $expected = [
  1441. 1 => 'mariano',
  1442. 2 => 'nate',
  1443. 3 => 'larry',
  1444. 4 => 'garrett',
  1445. ];
  1446. $this->assertSame($expected, $query->toArray());
  1447. });
  1448. }
  1449. /**
  1450. * test that find('list') does not auto add fields to select if using virtual properties
  1451. */
  1452. public function testFindListWithVirtualField(): void
  1453. {
  1454. $table = new Table([
  1455. 'table' => 'users',
  1456. 'connection' => $this->connection,
  1457. 'entityClass' => VirtualUser::class,
  1458. ]);
  1459. $table->setDisplayField('bonus');
  1460. $query = $table
  1461. ->find('list')
  1462. ->orderBy('id');
  1463. $this->assertEmpty($query->clause('select'));
  1464. $expected = [
  1465. 1 => 'bonus',
  1466. 2 => 'bonus',
  1467. 3 => 'bonus',
  1468. 4 => 'bonus',
  1469. ];
  1470. $this->assertSame($expected, $query->toArray());
  1471. $query = $table->find('list', groupField: 'odd');
  1472. $this->assertEmpty($query->clause('select'));
  1473. }
  1474. /**
  1475. * Test find('list') with value field from associated table
  1476. */
  1477. public function testFindListWithAssociatedTable(): void
  1478. {
  1479. $articles = new Table([
  1480. 'table' => 'articles',
  1481. 'connection' => $this->connection,
  1482. ]);
  1483. $articles->belongsTo('Authors');
  1484. $query = $articles->find('list', valueField: 'author.name')
  1485. ->contain(['Authors'])
  1486. ->orderBy('articles.id');
  1487. $this->assertEmpty($query->clause('select'));
  1488. $expected = [
  1489. 1 => 'mariano',
  1490. 2 => 'larry',
  1491. 3 => 'mariano',
  1492. ];
  1493. $this->assertSame($expected, $query->toArray());
  1494. }
  1495. /**
  1496. * Test find('list') called with option array instead of named args for backwards compatility
  1497. *
  1498. * @return void
  1499. * @deprecated
  1500. */
  1501. #[WithoutErrorHandler]
  1502. public function testFindListWithArray(): void
  1503. {
  1504. $this->deprecated(function () {
  1505. $articles = new Table([
  1506. 'table' => 'articles',
  1507. 'connection' => $this->connection,
  1508. ]);
  1509. $articles->belongsTo('Authors');
  1510. $query = $articles->find('list', ['valueField' => 'author.name'])
  1511. ->contain(['Authors'])
  1512. ->orderBy('articles.id');
  1513. $this->assertEmpty($query->clause('select'));
  1514. $expected = [
  1515. 1 => 'mariano',
  1516. 2 => 'larry',
  1517. 3 => 'mariano',
  1518. ];
  1519. $this->assertSame($expected, $query->toArray());
  1520. });
  1521. }
  1522. /**
  1523. * Test the default entityClass.
  1524. */
  1525. public function testEntityClassDefault(): void
  1526. {
  1527. $table = new Table();
  1528. $this->assertSame('Cake\ORM\Entity', $table->getEntityClass());
  1529. }
  1530. /**
  1531. * Tests that using a simple string for entityClass will try to
  1532. * load the class from the App namespace
  1533. */
  1534. public function testTableClassInApp(): void
  1535. {
  1536. $class = get_class($this->createMock('Cake\ORM\Entity'));
  1537. if (!class_exists('TestApp\Model\Entity\TestUser')) {
  1538. class_alias($class, 'TestApp\Model\Entity\TestUser');
  1539. }
  1540. $table = new Table();
  1541. $this->assertSame($table, $table->setEntityClass('TestUser'));
  1542. $this->assertSame('TestApp\Model\Entity\TestUser', $table->getEntityClass());
  1543. }
  1544. /**
  1545. * Test that entity class inflection works for compound nouns
  1546. */
  1547. public function testEntityClassInflection(): void
  1548. {
  1549. $class = get_class($this->createMock('Cake\ORM\Entity'));
  1550. if (!class_exists('TestApp\Model\Entity\CustomCookie')) {
  1551. class_alias($class, 'TestApp\Model\Entity\CustomCookie');
  1552. }
  1553. $table = $this->getTableLocator()->get('CustomCookies');
  1554. $this->assertSame('TestApp\Model\Entity\CustomCookie', $table->getEntityClass());
  1555. if (!class_exists('TestApp\Model\Entity\Address')) {
  1556. class_alias($class, 'TestApp\Model\Entity\Address');
  1557. }
  1558. $table = $this->getTableLocator()->get('Addresses');
  1559. $this->assertSame('TestApp\Model\Entity\Address', $table->getEntityClass());
  1560. }
  1561. /**
  1562. * Tests that using a simple string for entityClass will try to
  1563. * load the class from the Plugin namespace when using plugin notation
  1564. */
  1565. public function testTableClassInPlugin(): void
  1566. {
  1567. $class = get_class($this->createMock('Cake\ORM\Entity'));
  1568. if (!class_exists('MyPlugin\Model\Entity\SuperUser')) {
  1569. class_alias($class, 'MyPlugin\Model\Entity\SuperUser');
  1570. }
  1571. $table = new Table();
  1572. $this->assertSame($table, $table->setEntityClass('MyPlugin.SuperUser'));
  1573. $this->assertSame(
  1574. 'MyPlugin\Model\Entity\SuperUser',
  1575. $table->getEntityClass()
  1576. );
  1577. }
  1578. /**
  1579. * Tests that using a simple string for entityClass will throw an exception
  1580. * when the class does not exist in the namespace
  1581. */
  1582. public function testTableClassNonExistent(): void
  1583. {
  1584. $this->expectException(MissingEntityException::class);
  1585. $this->expectExceptionMessage('Entity class `FooUser` could not be found.');
  1586. $table = new Table();
  1587. $table->setEntityClass('FooUser');
  1588. }
  1589. /**
  1590. * Tests getting the entityClass based on conventions for the entity
  1591. * namespace
  1592. */
  1593. public function testTableClassConventionForAPP(): void
  1594. {
  1595. $table = new ArticlesTable();
  1596. $this->assertSame('TestApp\Model\Entity\Article', $table->getEntityClass());
  1597. }
  1598. /**
  1599. * Tests setting a entity class object using the setter method
  1600. */
  1601. public function testSetEntityClass(): void
  1602. {
  1603. $table = new Table();
  1604. $class = '\\' . get_class($this->createMock('Cake\ORM\Entity'));
  1605. $this->assertSame($table, $table->setEntityClass($class));
  1606. $this->assertSame($class, $table->getEntityClass());
  1607. }
  1608. /**
  1609. * Proves that associations, even though they are lazy loaded, will fetch
  1610. * records using the correct table class and hydrate with the correct entity
  1611. */
  1612. public function testReciprocalBelongsToLoading(): void
  1613. {
  1614. $table = new ArticlesTable([
  1615. 'connection' => $this->connection,
  1616. ]);
  1617. $result = $table->find('all')->contain(['Authors'])->first();
  1618. $this->assertInstanceOf('TestApp\Model\Entity\Author', $result->author);
  1619. }
  1620. /**
  1621. * Proves that associations, even though they are lazy loaded, will fetch
  1622. * records using the correct table class and hydrate with the correct entity
  1623. */
  1624. public function testReciprocalHasManyLoading(): void
  1625. {
  1626. $table = new ArticlesTable([
  1627. 'connection' => $this->connection,
  1628. ]);
  1629. $result = $table->find('all')->contain(['Authors' => ['Articles']])->first();
  1630. $this->assertCount(2, $result->author->articles);
  1631. foreach ($result->author->articles as $article) {
  1632. $this->assertInstanceOf('TestApp\Model\Entity\Article', $article);
  1633. }
  1634. }
  1635. /**
  1636. * Tests that the correct table and entity are loaded for the join association in
  1637. * a belongsToMany setup
  1638. */
  1639. public function testReciprocalBelongsToMany(): void
  1640. {
  1641. $table = new ArticlesTable([
  1642. 'connection' => $this->connection,
  1643. ]);
  1644. $result = $table->find('all')->contain(['Tags'])->first();
  1645. $this->assertInstanceOf('TestApp\Model\Entity\Tag', $result->tags[0]);
  1646. $this->assertInstanceOf(
  1647. 'TestApp\Model\Entity\ArticlesTag',
  1648. $result->tags[0]->_joinData
  1649. );
  1650. }
  1651. /**
  1652. * Tests that recently fetched entities are always clean
  1653. */
  1654. public function testFindCleanEntities(): void
  1655. {
  1656. $table = new ArticlesTable([
  1657. 'connection' => $this->connection,
  1658. ]);
  1659. $results = $table->find('all')->contain(['Tags', 'Authors'])->toArray();
  1660. $this->assertCount(3, $results);
  1661. foreach ($results as $article) {
  1662. $this->assertFalse($article->isDirty('id'));
  1663. $this->assertFalse($article->isDirty('title'));
  1664. $this->assertFalse($article->isDirty('author_id'));
  1665. $this->assertFalse($article->isDirty('body'));
  1666. $this->assertFalse($article->isDirty('published'));
  1667. $this->assertFalse($article->isDirty('author'));
  1668. $this->assertFalse($article->author->isDirty('id'));
  1669. $this->assertFalse($article->author->isDirty('name'));
  1670. $this->assertFalse($article->isDirty('tag'));
  1671. if ($article->tag) {
  1672. $this->assertFalse($article->tag[0]->_joinData->isDirty('tag_id'));
  1673. }
  1674. }
  1675. }
  1676. /**
  1677. * Tests that recently fetched entities are marked as not new
  1678. */
  1679. public function testFindPersistedEntities(): void
  1680. {
  1681. $table = new ArticlesTable([
  1682. 'connection' => $this->connection,
  1683. ]);
  1684. $results = $table->find('all')->contain(['Tags', 'Authors'])->toArray();
  1685. $this->assertCount(3, $results);
  1686. foreach ($results as $article) {
  1687. $this->assertFalse($article->isNew());
  1688. foreach ((array)$article->tag as $tag) {
  1689. $this->assertFalse($tag->isNew());
  1690. $this->assertFalse($tag->_joinData->isNew());
  1691. }
  1692. }
  1693. }
  1694. /**
  1695. * Tests the exists function
  1696. */
  1697. public function testExists(): void
  1698. {
  1699. $table = $this->getTableLocator()->get('users');
  1700. $this->assertTrue($table->exists(['id' => 1]));
  1701. $this->assertFalse($table->exists(['id' => 501]));
  1702. $this->assertTrue($table->exists(['id' => 3, 'username' => 'larry']));
  1703. }
  1704. /**
  1705. * Test adding a behavior to a table.
  1706. */
  1707. public function testAddBehavior(): void
  1708. {
  1709. $mock = $this->getMockBuilder('Cake\ORM\BehaviorRegistry')
  1710. ->disableOriginalConstructor()
  1711. ->getMock();
  1712. $mock->expects($this->once())
  1713. ->method('load')
  1714. ->with('Sluggable');
  1715. $table = new Table([
  1716. 'table' => 'articles',
  1717. 'behaviors' => $mock,
  1718. ]);
  1719. $result = $table->addBehavior('Sluggable');
  1720. $this->assertSame($table, $result);
  1721. }
  1722. /**
  1723. * Test adding a plugin behavior to a table.
  1724. */
  1725. public function testAddBehaviorPlugin(): void
  1726. {
  1727. $table = new Table([
  1728. 'table' => 'articles',
  1729. ]);
  1730. $result = $table->addBehavior('TestPlugin.PersisterOne', ['some' => 'key']);
  1731. $this->assertSame(['PersisterOne'], $result->behaviors()->loaded());
  1732. $className = $result->behaviors()->get('PersisterOne')->getConfig('className');
  1733. $this->assertSame('TestPlugin.PersisterOne', $className);
  1734. }
  1735. /**
  1736. * Test adding a behavior that is a duplicate.
  1737. */
  1738. public function testAddBehaviorDuplicate(): void
  1739. {
  1740. $table = new Table(['table' => 'articles']);
  1741. $this->assertSame($table, $table->addBehavior('Sluggable', ['test' => 'value']));
  1742. $this->assertSame($table, $table->addBehavior('Sluggable', ['test' => 'value']));
  1743. try {
  1744. $table->addBehavior('Sluggable', ['thing' => 'thing']);
  1745. $this->fail('No exception raised');
  1746. } catch (RuntimeException $e) {
  1747. $this->assertStringContainsString('The `Sluggable` alias has already been loaded', $e->getMessage());
  1748. }
  1749. }
  1750. /**
  1751. * Test removing a behavior from a table.
  1752. */
  1753. public function testRemoveBehavior(): void
  1754. {
  1755. $mock = $this->getMockBuilder('Cake\ORM\BehaviorRegistry')
  1756. ->disableOriginalConstructor()
  1757. ->getMock();
  1758. $mock->expects($this->once())
  1759. ->method('unload')
  1760. ->with('Sluggable');
  1761. $table = new Table([
  1762. 'table' => 'articles',
  1763. 'behaviors' => $mock,
  1764. ]);
  1765. $result = $table->removeBehavior('Sluggable');
  1766. $this->assertSame($table, $result);
  1767. }
  1768. /**
  1769. * Test adding multiple behaviors to a table.
  1770. */
  1771. public function testAddBehaviors(): void
  1772. {
  1773. $table = new Table(['table' => 'comments']);
  1774. $behaviors = [
  1775. 'Sluggable',
  1776. 'Timestamp' => [
  1777. 'events' => [
  1778. 'Model.beforeSave' => [
  1779. 'created' => 'new',
  1780. 'updated' => 'always',
  1781. ],
  1782. ],
  1783. ],
  1784. ];
  1785. $this->assertSame($table, $table->addBehaviors($behaviors));
  1786. $this->assertTrue($table->behaviors()->has('Sluggable'));
  1787. $this->assertTrue($table->behaviors()->has('Timestamp'));
  1788. $this->assertSame(
  1789. $behaviors['Timestamp']['events'],
  1790. $table->behaviors()->get('Timestamp')->getConfig('events')
  1791. );
  1792. }
  1793. /**
  1794. * Test getting a behavior instance from a table.
  1795. */
  1796. public function testBehaviors(): void
  1797. {
  1798. $table = $this->getTableLocator()->get('article');
  1799. $result = $table->behaviors();
  1800. $this->assertInstanceOf('Cake\ORM\BehaviorRegistry', $result);
  1801. }
  1802. /**
  1803. * Test that the getBehavior() method retrieves a behavior from the table registry.
  1804. */
  1805. public function testGetBehavior(): void
  1806. {
  1807. $table = new Table(['table' => 'comments']);
  1808. $table->addBehavior('Sluggable');
  1809. $this->assertSame($table->behaviors()->get('Sluggable'), $table->getBehavior('Sluggable'));
  1810. }
  1811. /**
  1812. * Test that the getBehavior() method will throw an exception when you try to
  1813. * get a behavior that does not exist.
  1814. */
  1815. public function testGetBehaviorThrowsExceptionForMissingBehavior(): void
  1816. {
  1817. $table = new Table(['table' => 'comments']);
  1818. $this->expectException(InvalidArgumentException::class);
  1819. $this->expectExceptionMessage('The `Sluggable` behavior is not defined on `' . $table::class . '`.');
  1820. $this->assertFalse($table->hasBehavior('Sluggable'));
  1821. $table->getBehavior('Sluggable');
  1822. }
  1823. /**
  1824. * Ensure exceptions are raised on missing behaviors.
  1825. */
  1826. public function testAddBehaviorMissing(): void
  1827. {
  1828. $this->expectException(MissingBehaviorException::class);
  1829. $table = $this->getTableLocator()->get('article');
  1830. $this->assertNull($table->addBehavior('NopeNotThere'));
  1831. }
  1832. /**
  1833. * Test mixin methods from behaviors.
  1834. */
  1835. public function testCallBehaviorMethod(): void
  1836. {
  1837. $table = $this->getTableLocator()->get('article');
  1838. $table->addBehavior('Sluggable');
  1839. $this->assertSame('some-value', $table->slugify('some value'));
  1840. }
  1841. /**
  1842. * Test you can alias a behavior method
  1843. */
  1844. public function testCallBehaviorAliasedMethod(): void
  1845. {
  1846. $table = $this->getTableLocator()->get('article');
  1847. $table->addBehavior('Sluggable', ['implementedMethods' => ['wednesday' => 'slugify']]);
  1848. $this->assertSame('some-value', $table->wednesday('some value'));
  1849. }
  1850. /**
  1851. * Test finder methods from behaviors.
  1852. */
  1853. public function testCallBehaviorFinder(): void
  1854. {
  1855. $table = $this->getTableLocator()->get('articles');
  1856. $table->addBehavior('Sluggable');
  1857. $query = $table->find('noSlug');
  1858. $this->assertInstanceOf('Cake\ORM\Query', $query);
  1859. $this->assertNotEmpty($query->clause('where'));
  1860. }
  1861. /**
  1862. * testCallBehaviorAliasedFinder
  1863. */
  1864. public function testCallBehaviorAliasedFinder(): void
  1865. {
  1866. $table = $this->getTableLocator()->get('articles');
  1867. $table->addBehavior('Sluggable', ['implementedFinders' => ['special' => 'findNoSlug']]);
  1868. $query = $table->find('special');
  1869. $this->assertInstanceOf('Cake\ORM\Query', $query);
  1870. $this->assertNotEmpty($query->clause('where'));
  1871. }
  1872. /**
  1873. * Tests that it is possible to insert a new row using the save method
  1874. *
  1875. * @group save
  1876. */
  1877. public function testSaveNewEntity(): void
  1878. {
  1879. $entity = new Entity([
  1880. 'username' => 'superuser',
  1881. 'password' => 'root',
  1882. 'created' => new DateTime('2013-10-10 00:00'),
  1883. 'updated' => new DateTime('2013-10-10 00:00'),
  1884. ]);
  1885. $table = $this->getTableLocator()->get('users');
  1886. $this->assertSame($entity, $table->save($entity));
  1887. $this->assertEquals($entity->id, self::$nextUserId);
  1888. $row = $table->find()->where(['id' => self::$nextUserId])->first();
  1889. $this->assertEquals($entity->toArray(), $row->toArray());
  1890. }
  1891. /**
  1892. * Test that saving a new empty entity does nothing.
  1893. *
  1894. * @group save
  1895. */
  1896. public function testSaveNewEmptyEntity(): void
  1897. {
  1898. $entity = new Entity();
  1899. $table = $this->getTableLocator()->get('users');
  1900. $this->assertFalse($table->save($entity));
  1901. }
  1902. /**
  1903. * Test that saving a new empty entity does not call exists.
  1904. *
  1905. * @group save
  1906. */
  1907. public function testSaveNewEntityNoExists(): void
  1908. {
  1909. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  1910. $table = $this->getMockBuilder(Table::class)
  1911. ->onlyMethods(['exists'])
  1912. ->setConstructorArgs([[
  1913. 'connection' => $this->connection,
  1914. 'alias' => 'Users',
  1915. 'table' => 'users',
  1916. ]])
  1917. ->getMock();
  1918. $entity = $table->newEntity(['username' => 'mark']);
  1919. $this->assertTrue($entity->isNew());
  1920. $table->expects($this->never())
  1921. ->method('exists');
  1922. $this->assertSame($entity, $table->save($entity));
  1923. }
  1924. /**
  1925. * Test that saving a new entity with a Primary Key set does call exists.
  1926. *
  1927. * @group save
  1928. */
  1929. public function testSavePrimaryKeyEntityExists(): void
  1930. {
  1931. $this->skipIfSqlServer();
  1932. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  1933. $table = $this->getMockBuilder(Table::class)
  1934. ->onlyMethods(['exists'])
  1935. ->setConstructorArgs([[
  1936. 'connection' => $this->connection,
  1937. 'alias' => 'Users',
  1938. 'table' => 'users',
  1939. ]])
  1940. ->getMock();
  1941. $entity = $table->newEntity(['id' => 20, 'username' => 'mark']);
  1942. $this->assertTrue($entity->isNew());
  1943. $table->expects($this->once())->method('exists');
  1944. $this->assertSame($entity, $table->save($entity));
  1945. }
  1946. /**
  1947. * Test that saving a new entity with a Primary Key set does not call exists when checkExisting is false.
  1948. *
  1949. * @group save
  1950. */
  1951. public function testSavePrimaryKeyEntityNoExists(): void
  1952. {
  1953. $this->skipIfSqlServer();
  1954. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  1955. $table = $this->getMockBuilder(Table::class)
  1956. ->onlyMethods(['exists'])
  1957. ->setConstructorArgs([[
  1958. 'connection' => $this->connection,
  1959. 'alias' => 'Users',
  1960. 'table' => 'users',
  1961. ]])
  1962. ->getMock();
  1963. $entity = $table->newEntity(['id' => 20, 'username' => 'mark']);
  1964. $this->assertTrue($entity->isNew());
  1965. $table->expects($this->never())->method('exists');
  1966. $this->assertSame($entity, $table->save($entity, ['checkExisting' => false]));
  1967. }
  1968. /**
  1969. * Tests that saving an entity will filter out properties that
  1970. * are not present in the table schema when saving
  1971. *
  1972. * @group save
  1973. */
  1974. public function testSaveEntityOnlySchemaFields(): void
  1975. {
  1976. $entity = new Entity([
  1977. 'username' => 'superuser',
  1978. 'password' => 'root',
  1979. 'crazyness' => 'super crazy value',
  1980. 'created' => new DateTime('2013-10-10 00:00'),
  1981. 'updated' => new DateTime('2013-10-10 00:00'),
  1982. ]);
  1983. $table = $this->getTableLocator()->get('users');
  1984. $this->assertSame($entity, $table->save($entity));
  1985. $this->assertEquals($entity->id, self::$nextUserId);
  1986. $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
  1987. $entity->unset('crazyness');
  1988. $this->assertEquals($entity->toArray(), $row->toArray());
  1989. }
  1990. /**
  1991. * Tests that it is possible to modify data from the beforeSave callback
  1992. *
  1993. * @group save
  1994. */
  1995. public function testBeforeSaveModifyData(): void
  1996. {
  1997. $table = $this->getTableLocator()->get('users');
  1998. $data = new Entity([
  1999. 'username' => 'superuser',
  2000. 'created' => new DateTime('2013-10-10 00:00'),
  2001. 'updated' => new DateTime('2013-10-10 00:00'),
  2002. ]);
  2003. $listener = function ($event, EntityInterface $entity, $options) use ($data): void {
  2004. $this->assertSame($data, $entity);
  2005. $entity->set('password', 'foo');
  2006. };
  2007. $table->getEventManager()->on('Model.beforeSave', $listener);
  2008. $this->assertSame($data, $table->save($data));
  2009. $this->assertEquals($data->id, self::$nextUserId);
  2010. $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
  2011. $this->assertSame('foo', $row->get('password'));
  2012. }
  2013. /**
  2014. * Tests that it is possible to modify the options array in beforeSave
  2015. *
  2016. * @group save
  2017. */
  2018. public function testBeforeSaveModifyOptions(): void
  2019. {
  2020. $table = $this->getTableLocator()->get('users');
  2021. $data = new Entity([
  2022. 'username' => 'superuser',
  2023. 'password' => 'foo',
  2024. 'created' => new DateTime('2013-10-10 00:00'),
  2025. 'updated' => new DateTime('2013-10-10 00:00'),
  2026. ]);
  2027. $listener1 = function ($event, $entity, $options): void {
  2028. $options['crazy'] = true;
  2029. };
  2030. $listener2 = function ($event, $entity, $options): void {
  2031. $this->assertTrue($options['crazy']);
  2032. };
  2033. $table->getEventManager()->on('Model.beforeSave', $listener1);
  2034. $table->getEventManager()->on('Model.beforeSave', $listener2);
  2035. $this->assertSame($data, $table->save($data));
  2036. $this->assertEquals($data->id, self::$nextUserId);
  2037. $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
  2038. $this->assertEquals($data->toArray(), $row->toArray());
  2039. }
  2040. /**
  2041. * Tests that it is possible to stop the saving altogether, without implying
  2042. * the save operation failed
  2043. *
  2044. * @group save
  2045. */
  2046. public function testBeforeSaveStopEvent(): void
  2047. {
  2048. $table = $this->getTableLocator()->get('users');
  2049. $data = new Entity([
  2050. 'username' => 'superuser',
  2051. 'created' => new DateTime('2013-10-10 00:00'),
  2052. 'updated' => new DateTime('2013-10-10 00:00'),
  2053. ]);
  2054. $listener = function (EventInterface $event, $entity) {
  2055. $event->stopPropagation();
  2056. return $entity;
  2057. };
  2058. $table->getEventManager()->on('Model.beforeSave', $listener);
  2059. $this->assertSame($data, $table->save($data));
  2060. $this->assertNull($data->id);
  2061. $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
  2062. $this->assertNull($row);
  2063. }
  2064. /**
  2065. * Tests that if beforeSave event is stopped and callback doesn't return any
  2066. * value then save() returns false.
  2067. *
  2068. * @group save
  2069. */
  2070. public function testBeforeSaveStopEventWithNoResult(): void
  2071. {
  2072. $table = $this->getTableLocator()->get('users');
  2073. $data = new Entity([
  2074. 'username' => 'superuser',
  2075. 'created' => new DateTime('2013-10-10 00:00'),
  2076. 'updated' => new DateTime('2013-10-10 00:00'),
  2077. ]);
  2078. $listener = function (EventInterface $event, $entity): void {
  2079. $event->stopPropagation();
  2080. };
  2081. $table->getEventManager()->on('Model.beforeSave', $listener);
  2082. $this->assertFalse($table->save($data));
  2083. }
  2084. /**
  2085. * @group save
  2086. */
  2087. public function testBeforeSaveException(): void
  2088. {
  2089. $this->expectException(AssertionError::class);
  2090. $this->expectExceptionMessage('The beforeSave callback must return `false` or `EntityInterface` instance. Got `int` instead.');
  2091. $table = $this->getTableLocator()->get('users');
  2092. $data = new Entity([
  2093. 'username' => 'superuser',
  2094. 'created' => new DateTime('2013-10-10 00:00'),
  2095. 'updated' => new DateTime('2013-10-10 00:00'),
  2096. ]);
  2097. $listener = function (EventInterface $event, $entity) {
  2098. $event->stopPropagation();
  2099. return 1;
  2100. };
  2101. $table->getEventManager()->on('Model.beforeSave', $listener);
  2102. $table->save($data);
  2103. }
  2104. /**
  2105. * Asserts that afterSave callback is called on successful save
  2106. *
  2107. * @group save
  2108. */
  2109. public function testAfterSave(): void
  2110. {
  2111. $table = $this->getTableLocator()->get('users');
  2112. $data = $table->get(1);
  2113. $data->username = 'newusername';
  2114. $called = false;
  2115. $listener = function ($e, EntityInterface $entity, $options) use ($data, &$called): void {
  2116. $this->assertSame($data, $entity);
  2117. $this->assertTrue($entity->isDirty());
  2118. $called = true;
  2119. };
  2120. $table->getEventManager()->on('Model.afterSave', $listener);
  2121. $calledAfterCommit = false;
  2122. $listenerAfterCommit = function ($e, EntityInterface $entity, $options) use ($data, &$calledAfterCommit): void {
  2123. $this->assertSame($data, $entity);
  2124. $this->assertTrue($entity->isDirty());
  2125. $this->assertNotSame($data->get('username'), $data->getOriginal('username'));
  2126. $calledAfterCommit = true;
  2127. };
  2128. $table->getEventManager()->on('Model.afterSaveCommit', $listenerAfterCommit);
  2129. $this->assertSame($data, $table->save($data));
  2130. $this->assertTrue($called);
  2131. $this->assertTrue($calledAfterCommit);
  2132. }
  2133. /**
  2134. * Asserts that afterSaveCommit is also triggered for non-atomic saves
  2135. */
  2136. public function testAfterSaveCommitForNonAtomic(): void
  2137. {
  2138. $table = $this->getTableLocator()->get('users');
  2139. $data = new Entity([
  2140. 'username' => 'superuser',
  2141. 'created' => new DateTime('2013-10-10 00:00'),
  2142. 'updated' => new DateTime('2013-10-10 00:00'),
  2143. ]);
  2144. $called = false;
  2145. $listener = function ($e, $entity, $options) use ($data, &$called): void {
  2146. $this->assertSame($data, $entity);
  2147. $called = true;
  2148. };
  2149. $table->getEventManager()->on('Model.afterSave', $listener);
  2150. $calledAfterCommit = false;
  2151. $listenerAfterCommit = function ($e, $entity, $options) use (&$calledAfterCommit): void {
  2152. $calledAfterCommit = true;
  2153. };
  2154. $table->getEventManager()->on('Model.afterSaveCommit', $listenerAfterCommit);
  2155. $this->assertSame($data, $table->save($data, ['atomic' => false]));
  2156. $this->assertEquals($data->id, self::$nextUserId);
  2157. $this->assertTrue($called);
  2158. $this->assertTrue($calledAfterCommit);
  2159. }
  2160. /**
  2161. * Asserts the afterSaveCommit is not triggered if transaction is running.
  2162. */
  2163. public function testAfterSaveCommitWithTransactionRunning(): void
  2164. {
  2165. $table = $this->getTableLocator()->get('users');
  2166. $data = new Entity([
  2167. 'username' => 'superuser',
  2168. 'created' => new DateTime('2013-10-10 00:00'),
  2169. 'updated' => new DateTime('2013-10-10 00:00'),
  2170. ]);
  2171. $called = false;
  2172. $listener = function ($e, $entity, $options) use (&$called): void {
  2173. $called = true;
  2174. };
  2175. $table->getEventManager()->on('Model.afterSaveCommit', $listener);
  2176. $this->connection->begin();
  2177. $this->assertSame($data, $table->save($data));
  2178. $this->assertFalse($called);
  2179. $this->connection->commit();
  2180. }
  2181. /**
  2182. * Asserts the afterSaveCommit is not triggered if transaction is running.
  2183. */
  2184. public function testAfterSaveCommitWithNonAtomicAndTransactionRunning(): void
  2185. {
  2186. $table = $this->getTableLocator()->get('users');
  2187. $data = new Entity([
  2188. 'username' => 'superuser',
  2189. 'created' => new DateTime('2013-10-10 00:00'),
  2190. 'updated' => new DateTime('2013-10-10 00:00'),
  2191. ]);
  2192. $called = false;
  2193. $listener = function ($e, $entity, $options) use (&$called): void {
  2194. $called = true;
  2195. };
  2196. $table->getEventManager()->on('Model.afterSaveCommit', $listener);
  2197. $this->connection->begin();
  2198. $this->assertSame($data, $table->save($data, ['atomic' => false]));
  2199. $this->assertFalse($called);
  2200. $this->connection->commit();
  2201. }
  2202. /**
  2203. * Asserts that afterSave callback not is called on unsuccessful save
  2204. *
  2205. * @group save
  2206. */
  2207. public function testAfterSaveNotCalled(): void
  2208. {
  2209. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  2210. $table = $this->getMockBuilder(Table::class)
  2211. ->onlyMethods(['insertQuery'])
  2212. ->setConstructorArgs([['table' => 'users']])
  2213. ->getMock();
  2214. $query = $this->getMockBuilder(InsertQuery::class)
  2215. ->onlyMethods(['execute', 'addDefaultTypes'])
  2216. ->setConstructorArgs([$table])
  2217. ->getMock();
  2218. $statement = $this->createMock(StatementInterface::class);
  2219. $data = new Entity([
  2220. 'username' => 'superuser',
  2221. 'created' => new DateTime('2013-10-10 00:00'),
  2222. 'updated' => new DateTime('2013-10-10 00:00'),
  2223. ]);
  2224. $table->expects($this->once())->method('insertQuery')
  2225. ->willReturn($query);
  2226. $query->expects($this->once())->method('execute')
  2227. ->willReturn($statement);
  2228. $statement->expects($this->once())->method('rowCount')
  2229. ->willReturn(0);
  2230. $called = false;
  2231. $listener = function ($e, $entity, $options) use (&$called): void {
  2232. $called = true;
  2233. };
  2234. $table->getEventManager()->on('Model.afterSave', $listener);
  2235. $calledAfterCommit = false;
  2236. $listenerAfterCommit = function ($e, $entity, $options) use (&$calledAfterCommit): void {
  2237. $calledAfterCommit = true;
  2238. };
  2239. $table->getEventManager()->on('Model.afterSaveCommit', $listenerAfterCommit);
  2240. $this->assertFalse($table->save($data));
  2241. $this->assertFalse($called);
  2242. $this->assertFalse($calledAfterCommit);
  2243. }
  2244. /**
  2245. * Asserts that afterSaveCommit callback is triggered only for primary table
  2246. *
  2247. * @group save
  2248. */
  2249. public function testAfterSaveCommitTriggeredOnlyForPrimaryTable(): void
  2250. {
  2251. $entity = new Entity([
  2252. 'title' => 'A Title',
  2253. 'body' => 'A body',
  2254. ]);
  2255. $entity->author = new Entity([
  2256. 'name' => 'Jose',
  2257. ]);
  2258. $table = $this->getTableLocator()->get('articles');
  2259. $table->belongsTo('authors');
  2260. $calledForArticle = false;
  2261. $listenerForArticle = function ($e, $entity, $options) use (&$calledForArticle): void {
  2262. $calledForArticle = true;
  2263. };
  2264. $table->getEventManager()->on('Model.afterSaveCommit', $listenerForArticle);
  2265. $calledForAuthor = false;
  2266. $listenerForAuthor = function ($e, $entity, $options) use (&$calledForAuthor): void {
  2267. $calledForAuthor = true;
  2268. };
  2269. $table->authors->getEventManager()->on('Model.afterSaveCommit', $listenerForAuthor);
  2270. $this->assertSame($entity, $table->save($entity));
  2271. $this->assertFalse($entity->isNew());
  2272. $this->assertFalse($entity->author->isNew());
  2273. $this->assertTrue($calledForArticle);
  2274. $this->assertFalse($calledForAuthor);
  2275. }
  2276. /**
  2277. * Test that you cannot save rows without a primary key.
  2278. *
  2279. * @group save
  2280. */
  2281. public function testSaveNewErrorOnNoPrimaryKey(): void
  2282. {
  2283. $this->expectException(DatabaseException::class);
  2284. $this->expectExceptionMessage('Cannot insert row in `users` table, it has no primary key');
  2285. $entity = new Entity(['username' => 'superuser']);
  2286. $table = $this->getTableLocator()->get('users', [
  2287. 'schema' => [
  2288. 'id' => ['type' => 'integer'],
  2289. 'username' => ['type' => 'string'],
  2290. ],
  2291. ]);
  2292. $table->save($entity);
  2293. }
  2294. /**
  2295. * Tests that save is wrapped around a transaction
  2296. *
  2297. * @group save
  2298. */
  2299. public function testAtomicSave(): void
  2300. {
  2301. $config = ConnectionManager::getConfig('test');
  2302. $connection = $this->getMockBuilder('Cake\Database\Connection')
  2303. ->onlyMethods(['begin', 'commit', 'inTransaction'])
  2304. ->setConstructorArgs([['driver' => $this->connection->getDriver()] + $config])
  2305. ->getMock();
  2306. $table = new Table(['table' => 'users', 'connection' => $connection]);
  2307. $connection->expects($this->once())->method('begin');
  2308. $connection->expects($this->once())->method('commit');
  2309. $connection->expects($this->any())->method('inTransaction')->willReturn(true);
  2310. $data = new Entity([
  2311. 'username' => 'superuser',
  2312. 'created' => new DateTime('2013-10-10 00:00'),
  2313. 'updated' => new DateTime('2013-10-10 00:00'),
  2314. ]);
  2315. $this->assertSame($data, $table->save($data));
  2316. }
  2317. /**
  2318. * Tests that save will rollback the transaction in the case of an exception
  2319. *
  2320. * @group save
  2321. */
  2322. public function testAtomicSaveRollback(): void
  2323. {
  2324. $this->expectException(PDOException::class);
  2325. /** @var \Cake\Database\Connection|\PHPUnit\Framework\MockObject\MockObject $connection */
  2326. $connection = $this->getMockBuilder('Cake\Database\Connection')
  2327. ->onlyMethods(['begin', 'rollback'])
  2328. ->setConstructorArgs([['driver' => $this->connection->getDriver()] + ConnectionManager::getConfig('test')])
  2329. ->getMock();
  2330. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  2331. $table = $this->getMockBuilder(Table::class)
  2332. ->onlyMethods(['insertQuery', 'getConnection'])
  2333. ->setConstructorArgs([['table' => 'users']])
  2334. ->getMock();
  2335. $query = $this->getMockBuilder(InsertQuery::class)
  2336. ->onlyMethods(['execute', 'addDefaultTypes'])
  2337. ->setConstructorArgs([$table])
  2338. ->getMock();
  2339. $table->expects($this->any())->method('getConnection')
  2340. ->willReturn($connection);
  2341. $table->expects($this->once())->method('insertQuery')
  2342. ->willReturn($query);
  2343. $connection->expects($this->once())->method('begin');
  2344. $connection->expects($this->once())->method('rollback');
  2345. $query->expects($this->once())->method('execute')
  2346. ->will($this->throwException(new PDOException()));
  2347. $data = new Entity([
  2348. 'username' => 'superuser',
  2349. 'created' => new DateTime('2013-10-10 00:00'),
  2350. 'updated' => new DateTime('2013-10-10 00:00'),
  2351. ]);
  2352. $table->save($data);
  2353. }
  2354. /**
  2355. * Tests that save will rollback the transaction in the case of an exception
  2356. *
  2357. * @group save
  2358. */
  2359. public function testAtomicSaveRollbackOnFailure(): void
  2360. {
  2361. /** @var \Cake\Database\Connection|\PHPUnit\Framework\MockObject\MockObject $connection */
  2362. $connection = $this->getMockBuilder('Cake\Database\Connection')
  2363. ->onlyMethods(['begin', 'rollback'])
  2364. ->setConstructorArgs([['driver' => $this->connection->getDriver()] + ConnectionManager::getConfig('test')])
  2365. ->getMock();
  2366. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  2367. $table = $this->getMockBuilder(Table::class)
  2368. ->onlyMethods(['insertQuery', 'getConnection', 'exists'])
  2369. ->setConstructorArgs([['table' => 'users']])
  2370. ->getMock();
  2371. $query = $this->getMockBuilder(InsertQuery::class)
  2372. ->onlyMethods(['execute', 'addDefaultTypes'])
  2373. ->setConstructorArgs([$table])
  2374. ->getMock();
  2375. $table->expects($this->any())->method('getConnection')
  2376. ->willReturn($connection);
  2377. $table->expects($this->once())->method('insertQuery')
  2378. ->willReturn($query);
  2379. $statement = $this->createMock(StatementInterface::class);
  2380. $statement->expects($this->once())
  2381. ->method('rowCount')
  2382. ->willReturn(0);
  2383. $connection->expects($this->once())->method('begin');
  2384. $connection->expects($this->once())->method('rollback');
  2385. $query->expects($this->once())
  2386. ->method('execute')
  2387. ->willReturn($statement);
  2388. $data = new Entity([
  2389. 'username' => 'superuser',
  2390. 'created' => new DateTime('2013-10-10 00:00'),
  2391. 'updated' => new DateTime('2013-10-10 00:00'),
  2392. ]);
  2393. $table->save($data);
  2394. }
  2395. /**
  2396. * Tests that only the properties marked as dirty are actually saved
  2397. * to the database
  2398. *
  2399. * @group save
  2400. */
  2401. public function testSaveOnlyDirtyProperties(): void
  2402. {
  2403. $entity = new Entity([
  2404. 'username' => 'superuser',
  2405. 'password' => 'root',
  2406. 'created' => new DateTime('2013-10-10 00:00'),
  2407. 'updated' => new DateTime('2013-10-10 00:00'),
  2408. ]);
  2409. $entity->clean();
  2410. $entity->setDirty('username', true);
  2411. $entity->setDirty('created', true);
  2412. $entity->setDirty('updated', true);
  2413. $table = $this->getTableLocator()->get('users');
  2414. $this->assertSame($entity, $table->save($entity));
  2415. $this->assertEquals($entity->id, self::$nextUserId);
  2416. $row = $table->find('all')->where(['id' => self::$nextUserId])->first();
  2417. $entity->set('password', null);
  2418. $this->assertEquals($entity->toArray(), $row->toArray());
  2419. }
  2420. /**
  2421. * Tests that a recently saved entity is marked as clean
  2422. *
  2423. * @group save
  2424. */
  2425. public function testASavedEntityIsClean(): void
  2426. {
  2427. $entity = new Entity([
  2428. 'username' => 'superuser',
  2429. 'password' => 'root',
  2430. 'created' => new DateTime('2013-10-10 00:00'),
  2431. 'updated' => new DateTime('2013-10-10 00:00'),
  2432. ]);
  2433. $table = $this->getTableLocator()->get('users');
  2434. $this->assertSame($entity, $table->save($entity));
  2435. $this->assertFalse($entity->isDirty('usermane'));
  2436. $this->assertFalse($entity->isDirty('password'));
  2437. $this->assertFalse($entity->isDirty('created'));
  2438. $this->assertFalse($entity->isDirty('updated'));
  2439. }
  2440. /**
  2441. * Tests that a recently saved entity is marked as not new
  2442. *
  2443. * @group save
  2444. */
  2445. public function testASavedEntityIsNotNew(): void
  2446. {
  2447. $entity = new Entity([
  2448. 'username' => 'superuser',
  2449. 'password' => 'root',
  2450. 'created' => new DateTime('2013-10-10 00:00'),
  2451. 'updated' => new DateTime('2013-10-10 00:00'),
  2452. ]);
  2453. $table = $this->getTableLocator()->get('users');
  2454. $this->assertSame($entity, $table->save($entity));
  2455. $this->assertFalse($entity->isNew());
  2456. }
  2457. /**
  2458. * Tests that save can detect automatically if it needs to insert
  2459. * or update a row
  2460. *
  2461. * @group save
  2462. */
  2463. public function testSaveUpdateAuto(): void
  2464. {
  2465. $entity = new Entity([
  2466. 'id' => 2,
  2467. 'username' => 'baggins',
  2468. ]);
  2469. $table = $this->getTableLocator()->get('users');
  2470. $original = $table->find('all')->where(['id' => 2])->first();
  2471. $this->assertSame($entity, $table->save($entity));
  2472. $row = $table->find('all')->where(['id' => 2])->first();
  2473. $this->assertSame('baggins', $row->username);
  2474. $this->assertEquals($original->password, $row->password);
  2475. $this->assertEquals($original->created, $row->created);
  2476. $this->assertEquals($original->updated, $row->updated);
  2477. $this->assertFalse($entity->isNew());
  2478. $this->assertFalse($entity->isDirty('id'));
  2479. $this->assertFalse($entity->isDirty('username'));
  2480. }
  2481. /**
  2482. * Tests that beforeFind gets the correct isNew() state for the entity
  2483. */
  2484. public function testBeforeSaveGetsCorrectPersistance(): void
  2485. {
  2486. $entity = new Entity([
  2487. 'id' => 2,
  2488. 'username' => 'baggins',
  2489. ]);
  2490. $table = $this->getTableLocator()->get('users');
  2491. $called = false;
  2492. $listener = function (EventInterface $event, $entity) use (&$called): void {
  2493. $this->assertFalse($entity->isNew());
  2494. $called = true;
  2495. };
  2496. $table->getEventManager()->on('Model.beforeSave', $listener);
  2497. $this->assertSame($entity, $table->save($entity));
  2498. $this->assertTrue($called);
  2499. }
  2500. /**
  2501. * Tests that marking an entity as already persisted will prevent the save
  2502. * method from trying to infer the entity's actual status.
  2503. *
  2504. * @group save
  2505. */
  2506. public function testSaveUpdateWithHint(): void
  2507. {
  2508. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  2509. $table = $this->getMockBuilder(Table::class)
  2510. ->onlyMethods(['exists'])
  2511. ->setConstructorArgs([['table' => 'users', 'connection' => ConnectionManager::get('test')]])
  2512. ->getMock();
  2513. $entity = new Entity([
  2514. 'id' => 2,
  2515. 'username' => 'baggins',
  2516. ], ['markNew' => false]);
  2517. $this->assertFalse($entity->isNew());
  2518. $table->expects($this->never())->method('exists');
  2519. $this->assertSame($entity, $table->save($entity));
  2520. }
  2521. /**
  2522. * Tests that when updating the primary key is not passed to the list of
  2523. * attributes to change
  2524. *
  2525. * @group save
  2526. */
  2527. public function testSaveUpdatePrimaryKeyNotModified(): void
  2528. {
  2529. /** @var \Cake\Database\Connection|\PHPUnit\Framework\MockObject\MockObject $connection */
  2530. $connection = $this->getMockBuilder('Cake\Database\Connection')
  2531. ->onlyMethods(['run'])
  2532. ->setConstructorArgs([['driver' => $this->connection->getDriver()] + ConnectionManager::getConfig('test')])
  2533. ->getMock();
  2534. $table = $this->fetchTable('Users');
  2535. $table->setConnection($connection);
  2536. $statement = $this->getMockBuilder(StatementInterface::class)->getMock();
  2537. $statement->expects($this->once())
  2538. ->method('errorCode')
  2539. ->willReturn('00000');
  2540. $connection->expects($this->once())->method('run')
  2541. ->willReturn($statement);
  2542. $entity = new Entity([
  2543. 'id' => 2,
  2544. 'username' => 'baggins',
  2545. ], ['markNew' => false]);
  2546. $this->assertSame($entity, $table->save($entity));
  2547. }
  2548. /**
  2549. * Tests that passing only the primary key to save will not execute any queries
  2550. * but still return success
  2551. *
  2552. * @group save
  2553. */
  2554. public function testUpdateNoChange(): void
  2555. {
  2556. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  2557. $table = $this->getMockBuilder(Table::class)
  2558. ->onlyMethods(['query'])
  2559. ->setConstructorArgs([['table' => 'users', 'connection' => $this->connection]])
  2560. ->getMock();
  2561. $table->expects($this->never())->method('query');
  2562. $entity = new Entity([
  2563. 'id' => 2,
  2564. ], ['markNew' => false]);
  2565. $this->assertSame($entity, $table->save($entity));
  2566. }
  2567. /**
  2568. * Tests that passing only the primary key to save will not execute any queries
  2569. * but still return success
  2570. *
  2571. * @group save
  2572. * @group integration
  2573. */
  2574. public function testUpdateDirtyNoActualChanges(): void
  2575. {
  2576. $table = $this->getTableLocator()->get('Articles');
  2577. $entity = $table->get(1);
  2578. $entity->setAccess('*', true);
  2579. $entity->set($entity->toArray());
  2580. $this->assertSame($entity, $table->save($entity));
  2581. }
  2582. /**
  2583. * Tests that failing to pass a primary key to save will result in exception
  2584. *
  2585. * @group save
  2586. */
  2587. public function testUpdateNoPrimaryButOtherKeys(): void
  2588. {
  2589. $this->expectException(InvalidArgumentException::class);
  2590. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  2591. $table = $this->getMockBuilder(Table::class)
  2592. ->onlyMethods(['query'])
  2593. ->setConstructorArgs([['table' => 'users', 'connection' => $this->connection]])
  2594. ->getMock();
  2595. $table->expects($this->never())->method('query');
  2596. $entity = new Entity([
  2597. 'username' => 'mariano',
  2598. ], ['markNew' => false]);
  2599. $this->assertSame($entity, $table->save($entity));
  2600. }
  2601. /**
  2602. * Test saveMany() with entities array
  2603. */
  2604. public function testSaveManyArray(): void
  2605. {
  2606. $entities = [
  2607. new Entity(['name' => 'admad']),
  2608. new Entity(['name' => 'dakota']),
  2609. ];
  2610. $timesCalled = 0;
  2611. $listener = function ($e, $entity, $options) use (&$timesCalled): void {
  2612. $timesCalled++;
  2613. };
  2614. $table = $this->getTableLocator()
  2615. ->get('authors');
  2616. $table->getEventManager()
  2617. ->on('Model.afterSaveCommit', $listener);
  2618. $result = $table->saveMany($entities);
  2619. $this->assertSame($entities, $result);
  2620. $this->assertTrue(isset($result[0]->id));
  2621. foreach ($entities as $entity) {
  2622. $this->assertFalse($entity->isNew());
  2623. }
  2624. $this->assertSame(2, $timesCalled);
  2625. }
  2626. /**
  2627. * Test saveMany() with ResultSet instance
  2628. */
  2629. public function testSaveManyResultSet(): void
  2630. {
  2631. $table = $this->getTableLocator()->get('authors');
  2632. $table->Articles->setSort('Articles.id');
  2633. $entities = $table->find()
  2634. ->orderBy(['id' => 'ASC'])
  2635. ->contain(['Articles'])
  2636. ->all();
  2637. $entities->first()->name = 'admad';
  2638. $entities->first()->articles[0]->title = 'First Article Edited';
  2639. $listener = function (EventInterface $event, EntityInterface $entity, $options) {
  2640. if ($entity->id === 1) {
  2641. $this->assertTrue($entity->isDirty());
  2642. $this->assertSame('admad', $entity->name);
  2643. $this->assertSame('mariano', $entity->getOriginal('name'));
  2644. $this->assertSame('First Article Edited', $entity->articles[0]->title);
  2645. $this->assertSame('First Article', $entity->articles[0]->getOriginal('title'));
  2646. } else {
  2647. $this->assertFalse($entity->isDirty());
  2648. }
  2649. };
  2650. $table = $this->getTableLocator()
  2651. ->get('authors');
  2652. $table->getEventManager()
  2653. ->on('Model.afterSaveCommit', $listener);
  2654. $result = $table->saveMany($entities);
  2655. $this->assertSame($entities, $result);
  2656. $this->assertFalse($result->first()->isDirty());
  2657. $this->assertFalse($result->first()->articles[0]->isDirty());
  2658. $first = $table->find()
  2659. ->orderBy(['id' => 'ASC'])
  2660. ->first();
  2661. $this->assertSame('admad', $first->name);
  2662. }
  2663. /**
  2664. * Test saveMany() with failed save
  2665. */
  2666. public function testSaveManyFailed(): void
  2667. {
  2668. $table = $this->getTableLocator()->get('authors');
  2669. $expectedCount = $table->find()->count();
  2670. $entities = [
  2671. new Entity(['name' => 'mark']),
  2672. new Entity(['name' => 'jose']),
  2673. ];
  2674. $entities[1]->setErrors(['name' => ['message']]);
  2675. $result = $table->saveMany($entities);
  2676. $this->assertFalse($result);
  2677. $this->assertSame($expectedCount, $table->find()->count());
  2678. foreach ($entities as $entity) {
  2679. $this->assertTrue($entity->isNew());
  2680. }
  2681. }
  2682. /**
  2683. * Test saveMany() with failed save due to an exception
  2684. */
  2685. public function testSaveManyFailedWithException(): void
  2686. {
  2687. $table = $this->getTableLocator()
  2688. ->get('authors');
  2689. $entities = [
  2690. new Entity(['name' => 'mark']),
  2691. new Entity(['name' => 'jose']),
  2692. ];
  2693. $table->getEventManager()->on('Model.beforeSave', function (EventInterface $event, EntityInterface $entity): void {
  2694. if ($entity->name === 'jose') {
  2695. throw new Exception('Oh noes');
  2696. }
  2697. });
  2698. $this->expectException(Exception::class);
  2699. try {
  2700. $table->saveMany($entities);
  2701. } finally {
  2702. foreach ($entities as $entity) {
  2703. $this->assertTrue($entity->isNew());
  2704. }
  2705. }
  2706. }
  2707. /**
  2708. * Test saveManyOrFail() with entities array
  2709. */
  2710. public function testSaveManyOrFailArray(): void
  2711. {
  2712. $entities = [
  2713. new Entity(['name' => 'admad']),
  2714. new Entity(['name' => 'dakota']),
  2715. ];
  2716. $table = $this->getTableLocator()->get('authors');
  2717. $result = $table->saveManyOrFail($entities);
  2718. $this->assertSame($entities, $result);
  2719. $this->assertTrue(isset($result[0]->id));
  2720. foreach ($entities as $entity) {
  2721. $this->assertFalse($entity->isNew());
  2722. }
  2723. }
  2724. /**
  2725. * Test saveManyOrFail() with ResultSet instance
  2726. */
  2727. public function testSaveManyOrFailResultSet(): void
  2728. {
  2729. $table = $this->getTableLocator()->get('authors');
  2730. $entities = $table->find()
  2731. ->orderBy(['id' => 'ASC'])
  2732. ->all();
  2733. $entities->first()->name = 'admad';
  2734. $result = $table->saveManyOrFail($entities);
  2735. $this->assertSame($entities, $result);
  2736. $first = $table->find()
  2737. ->orderBy(['id' => 'ASC'])
  2738. ->first();
  2739. $this->assertSame('admad', $first->name);
  2740. }
  2741. /**
  2742. * Test saveManyOrFail() with failed save
  2743. */
  2744. public function testSaveManyOrFailFailed(): void
  2745. {
  2746. $table = $this->getTableLocator()->get('authors');
  2747. $entities = [
  2748. new Entity(['name' => 'mark']),
  2749. new Entity(['name' => 'jose']),
  2750. ];
  2751. $entities[1]->setErrors(['name' => ['message']]);
  2752. $this->expectException(PersistenceFailedException::class);
  2753. $table->saveManyOrFail($entities);
  2754. }
  2755. /**
  2756. * Test simple delete.
  2757. */
  2758. public function testDelete(): void
  2759. {
  2760. $table = $this->getTableLocator()->get('users');
  2761. $options = [
  2762. 'limit' => 1,
  2763. 'conditions' => [
  2764. 'username' => 'nate',
  2765. ],
  2766. ];
  2767. $query = $table->find('all', ...$options);
  2768. $entity = $query->first();
  2769. $result = $table->delete($entity);
  2770. $this->assertTrue($result);
  2771. $query = $table->find('all', ...$options);
  2772. $this->assertCount(0, $query->all(), 'Find should fail.');
  2773. }
  2774. /**
  2775. * Test delete with dependent records
  2776. */
  2777. public function testDeleteDependent(): void
  2778. {
  2779. $table = $this->getTableLocator()->get('authors');
  2780. $table->Articles->setDependent(true);
  2781. $entity = $table->get(1);
  2782. $table->delete($entity);
  2783. $articles = $table->getAssociation('Articles')->getTarget();
  2784. $query = $articles->find('all', conditions: ['author_id' => $entity->id]);
  2785. $this->assertNull($query->all()->first(), 'Should not find any rows.');
  2786. }
  2787. /**
  2788. * Test delete with dependent records
  2789. */
  2790. public function testDeleteDependentHasMany(): void
  2791. {
  2792. $table = $this->getTableLocator()->get('authors');
  2793. $table->Articles
  2794. ->setDependent(true)
  2795. ->setCascadeCallbacks(true);
  2796. $articles = $table->getAssociation('Articles')->getTarget();
  2797. $articles->getEventManager()->on('Model.buildRules', function ($event, $rules): void {
  2798. $rules->addDelete(function ($entity) {
  2799. if ($entity->author_id === 3) {
  2800. return false;
  2801. } else {
  2802. return true;
  2803. }
  2804. });
  2805. });
  2806. $entity = $table->get(1);
  2807. $result = $table->delete($entity);
  2808. $this->assertTrue($result);
  2809. $query = $articles->find('all', conditions: ['author_id' => $entity->id]);
  2810. $this->assertNull($query->all()->first(), 'Should not find any rows.');
  2811. $entity = $table->get(3);
  2812. $result = $table->delete($entity);
  2813. $this->assertFalse($result);
  2814. $query = $articles->find('all', conditions: ['author_id' => $entity->id]);
  2815. $this->assertFalse($query->all()->isEmpty(), 'Should find some rows.');
  2816. $table->associations()->get('Articles')->setCascadeCallbacks(false);
  2817. $entity = $table->get(2);
  2818. $result = $table->delete($entity);
  2819. $this->assertTrue($result);
  2820. }
  2821. /**
  2822. * Test delete with dependent = false does not cascade.
  2823. */
  2824. public function testDeleteNoDependentNoCascade(): void
  2825. {
  2826. $table = $this->getTableLocator()->get('authors');
  2827. $table->hasMany('article', [
  2828. 'dependent' => false,
  2829. ]);
  2830. $query = $table->find('all')->where(['id' => 1]);
  2831. $entity = $query->first();
  2832. $table->delete($entity);
  2833. $articles = $table->getAssociation('Articles')->getTarget();
  2834. $query = $articles->find('all')->where(['author_id' => $entity->id]);
  2835. $this->assertCount(2, $query->all(), 'Should find rows.');
  2836. }
  2837. /**
  2838. * Test delete with BelongsToMany
  2839. */
  2840. public function testDeleteBelongsToMany(): void
  2841. {
  2842. $table = $this->getTableLocator()->get('articles');
  2843. $table->belongsToMany('tag', [
  2844. 'foreignKey' => 'article_id',
  2845. 'joinTable' => 'articles_tags',
  2846. ]);
  2847. $query = $table->find('all')->where(['id' => 1]);
  2848. $entity = $query->first();
  2849. $table->delete($entity);
  2850. $junction = $table->getAssociation('tag')->junction();
  2851. $query = $junction->find('all')->where(['article_id' => 1]);
  2852. $this->assertNull($query->all()->first(), 'Should not find any rows.');
  2853. }
  2854. /**
  2855. * Test delete with dependent records belonging to an aliased
  2856. * belongsToMany association.
  2857. */
  2858. public function testDeleteDependentAliased(): void
  2859. {
  2860. $Authors = $this->getTableLocator()->get('authors');
  2861. $Authors->associations()->removeAll();
  2862. $Articles = $this->getTableLocator()->get('articles');
  2863. $Articles->associations()->removeAll();
  2864. $Authors->hasMany('AliasedArticles', [
  2865. 'className' => 'Articles',
  2866. 'dependent' => true,
  2867. 'cascadeCallbacks' => true,
  2868. ]);
  2869. $Articles->belongsToMany('Tags');
  2870. $author = $Authors->get(1);
  2871. $result = $Authors->delete($author);
  2872. $this->assertTrue($result);
  2873. }
  2874. /**
  2875. * Test that cascading associations are deleted first.
  2876. */
  2877. public function testDeleteAssociationsCascadingCallbacksOrder(): void
  2878. {
  2879. $sections = $this->getTableLocator()->get('Sections');
  2880. $members = $this->getTableLocator()->get('Members');
  2881. $sectionsMembers = $this->getTableLocator()->get('SectionsMembers');
  2882. $sections->belongsToMany('Members', [
  2883. 'joinTable' => 'sections_members',
  2884. ]);
  2885. $sections->hasMany('SectionsMembers', [
  2886. 'dependent' => true,
  2887. 'cascadeCallbacks' => true,
  2888. ]);
  2889. $sectionsMembers->belongsTo('Members');
  2890. $sectionsMembers->addBehavior('CounterCache', [
  2891. 'Members' => ['section_count'],
  2892. ]);
  2893. $member = $members->get(1);
  2894. $this->assertSame(2, $member->section_count);
  2895. $section = $sections->get(1);
  2896. $sections->delete($section);
  2897. $member = $members->get(1);
  2898. $this->assertSame(1, $member->section_count);
  2899. }
  2900. /**
  2901. * Test that primary record is not deleted if junction record deletion fails
  2902. * when cascadeCallbacks is enabled.
  2903. */
  2904. public function testDeleteBelongsToManyDependentFailure(): void
  2905. {
  2906. $sections = $this->getTableLocator()->get('Sections');
  2907. $sectionsMembers = $this->getTableLocator()->get('SectionsMembers');
  2908. $sectionsMembers->getEventManager()->on('Model.buildRules', function ($event, $rules): void {
  2909. $rules->addDelete(function () {
  2910. return false;
  2911. });
  2912. });
  2913. $sections->belongsToMany('Members', [
  2914. 'joinTable' => 'sections_members',
  2915. 'dependent' => true,
  2916. 'cascadeCallbacks' => true,
  2917. ]);
  2918. $section = $sections->get(1, contain: 'Members');
  2919. $this->assertSame(1, count($section->members));
  2920. $this->assertFalse($sections->delete($section));
  2921. $section = $sections->get(1, contain: 'Members');
  2922. $this->assertSame(1, count($section->members));
  2923. }
  2924. /**
  2925. * Test delete callbacks
  2926. */
  2927. public function testDeleteCallbacks(): void
  2928. {
  2929. $entity = new Entity(['id' => 1, 'name' => 'mark']);
  2930. $options = new ArrayObject(['atomic' => true, 'checkRules' => false, '_primary' => true]);
  2931. $mock = $this->getMockBuilder('Cake\Event\EventManager')->getMock();
  2932. $mock->expects($this->once())
  2933. ->method('on');
  2934. $mock->expects($this->exactly(4))
  2935. ->method('dispatch')
  2936. ->with(
  2937. ...self::withConsecutive(
  2938. [$this->anything()],
  2939. [$this->callback(function (EventInterface $event) use ($entity, $options) {
  2940. return $event->getName() === 'Model.beforeDelete' &&
  2941. $event->getData() == ['entity' => $entity, 'options' => $options];
  2942. })],
  2943. [
  2944. $this->callback(function (EventInterface $event) use ($entity, $options) {
  2945. return $event->getName() === 'Model.afterDelete' &&
  2946. $event->getData() == ['entity' => $entity, 'options' => $options];
  2947. }),
  2948. ],
  2949. [$this->callback(function (EventInterface $event) use ($entity, $options) {
  2950. return $event->getName() === 'Model.afterDeleteCommit' &&
  2951. $event->getData() == ['entity' => $entity, 'options' => $options];
  2952. })]
  2953. )
  2954. );
  2955. $table = $this->getTableLocator()->get('users', ['eventManager' => $mock]);
  2956. $entity->setNew(false);
  2957. $table->delete($entity, ['checkRules' => false]);
  2958. }
  2959. /**
  2960. * Test afterDeleteCommit is also called for non-atomic delete
  2961. */
  2962. public function testDeleteCallbacksNonAtomic(): void
  2963. {
  2964. $table = $this->getTableLocator()->get('users');
  2965. $data = $table->get(1);
  2966. $called = false;
  2967. $listener = function ($e, $entity, $options) use ($data, &$called): void {
  2968. $this->assertSame($data, $entity);
  2969. $called = true;
  2970. };
  2971. $table->getEventManager()->on('Model.afterDelete', $listener);
  2972. $calledAfterCommit = false;
  2973. $listenerAfterCommit = function ($e, $entity, $options) use (&$calledAfterCommit): void {
  2974. $calledAfterCommit = true;
  2975. };
  2976. $table->getEventManager()->on('Model.afterDeleteCommit', $listenerAfterCommit);
  2977. $table->delete($data, ['atomic' => false]);
  2978. $this->assertTrue($called);
  2979. $this->assertTrue($calledAfterCommit);
  2980. }
  2981. /**
  2982. * Test that afterDeleteCommit is only triggered for primary table
  2983. */
  2984. public function testAfterDeleteCommitTriggeredOnlyForPrimaryTable(): void
  2985. {
  2986. $table = $this->getTableLocator()->get('authors');
  2987. $table->Articles->setDependent(true);
  2988. $called = false;
  2989. $listener = function ($e, $entity, $options) use (&$called): void {
  2990. $called = true;
  2991. };
  2992. $table->getEventManager()->on('Model.afterDeleteCommit', $listener);
  2993. $called2 = false;
  2994. $listener = function ($e, $entity, $options) use (&$called2): void {
  2995. $called2 = true;
  2996. };
  2997. $table->Articles->getEventManager()->on('Model.afterDeleteCommit', $listener);
  2998. $entity = $table->get(1);
  2999. $this->assertTrue($table->delete($entity));
  3000. $this->assertTrue($called);
  3001. $this->assertFalse($called2);
  3002. }
  3003. /**
  3004. * Test delete beforeDelete can abort the delete.
  3005. */
  3006. public function testDeleteBeforeDeleteAbort(): void
  3007. {
  3008. $entity = new Entity(['id' => 1, 'name' => 'mark']);
  3009. $mock = $this->getMockBuilder('Cake\Event\EventManager')->getMock();
  3010. $mock->expects($this->any())
  3011. ->method('dispatch')
  3012. ->willReturnCallback(function (EventInterface $event) {
  3013. $event->stopPropagation();
  3014. return $event;
  3015. });
  3016. $table = $this->getTableLocator()->get('users', ['eventManager' => $mock]);
  3017. $entity->setNew(false);
  3018. $result = $table->delete($entity, ['checkRules' => false]);
  3019. $this->assertFalse($result);
  3020. }
  3021. /**
  3022. * Test delete beforeDelete return result
  3023. */
  3024. public function testDeleteBeforeDeleteReturnResult(): void
  3025. {
  3026. $entity = new Entity(['id' => 1, 'name' => 'mark']);
  3027. $mock = $this->getMockBuilder('Cake\Event\EventManager')->getMock();
  3028. $mock->expects($this->any())
  3029. ->method('dispatch')
  3030. ->willReturnCallback(function (EventInterface $event) {
  3031. $event->stopPropagation();
  3032. $event->setResult('got stopped');
  3033. return $event;
  3034. });
  3035. $table = $this->getTableLocator()->get('users', ['eventManager' => $mock]);
  3036. $entity->setNew(false);
  3037. $result = $table->delete($entity, ['checkRules' => false]);
  3038. $this->assertTrue($result);
  3039. }
  3040. /**
  3041. * Test deleting new entities does nothing.
  3042. */
  3043. public function testDeleteIsNew(): void
  3044. {
  3045. $entity = new Entity(['id' => 1, 'name' => 'mark']);
  3046. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $table */
  3047. $table = $this->getMockBuilder(Table::class)
  3048. ->onlyMethods(['query'])
  3049. ->setConstructorArgs([['connection' => $this->connection]])
  3050. ->getMock();
  3051. $table->expects($this->never())
  3052. ->method('query');
  3053. $entity->setNew(true);
  3054. $result = $table->delete($entity);
  3055. $this->assertFalse($result);
  3056. }
  3057. /**
  3058. * Test simple delete.
  3059. */
  3060. public function testDeleteMany(): void
  3061. {
  3062. $table = $this->getTableLocator()->get('users');
  3063. $entities = $table->find()->limit(2)->all()->toArray();
  3064. $this->assertCount(2, $entities);
  3065. $result = $table->deleteMany($entities);
  3066. $this->assertSame($entities, $result);
  3067. $count = $table->find()->where(['id IN' => Hash::extract($entities, '{n}.id')])->count();
  3068. $this->assertSame(0, $count, 'Find should not return > 0.');
  3069. }
  3070. /**
  3071. * Test simple delete.
  3072. */
  3073. public function testDeleteManyOrFail(): void
  3074. {
  3075. $table = $this->getTableLocator()->get('users');
  3076. $entities = $table->find()->limit(2)->all()->toArray();
  3077. $this->assertCount(2, $entities);
  3078. $result = $table->deleteManyOrFail($entities);
  3079. $this->assertSame($entities, $result);
  3080. $count = $table->find()->where(['id IN' => Hash::extract($entities, '{n}.id')])->count();
  3081. $this->assertSame(0, $count, 'Find should not return > 0.');
  3082. }
  3083. /**
  3084. * test hasField()
  3085. */
  3086. public function testHasField(): void
  3087. {
  3088. $table = $this->getTableLocator()->get('articles');
  3089. $this->assertFalse($table->hasField('nope'), 'Should not be there.');
  3090. $this->assertTrue($table->hasField('title'), 'Should be there.');
  3091. $this->assertTrue($table->hasField('body'), 'Should be there.');
  3092. }
  3093. /**
  3094. * Tests that there exists a default validator
  3095. */
  3096. public function testValidatorDefault(): void
  3097. {
  3098. $table = new Table();
  3099. $validator = $table->getValidator();
  3100. $this->assertSame($table, $validator->getProvider('table'));
  3101. $this->assertInstanceOf('Cake\Validation\Validator', $validator);
  3102. $default = $table->getValidator('default');
  3103. $this->assertSame($validator, $default);
  3104. }
  3105. /**
  3106. * Tests that there exists a validator defined in a behavior.
  3107. */
  3108. public function testValidatorBehavior(): void
  3109. {
  3110. $table = new Table();
  3111. $table->addBehavior('Validation');
  3112. $validator = $table->getValidator('Behavior');
  3113. $set = $validator->field('name');
  3114. $this->assertArrayHasKey('behaviorRule', $set);
  3115. }
  3116. /**
  3117. * Tests that a InvalidArgumentException is thrown if the custom validator method does not exist.
  3118. */
  3119. public function testValidatorWithMissingMethod(): void
  3120. {
  3121. $this->expectException(InvalidArgumentException::class);
  3122. $this->expectExceptionMessage('The `Cake\ORM\Table::validationMissing()` validation method does not exists.');
  3123. $table = new Table();
  3124. $table->getValidator('missing');
  3125. }
  3126. /**
  3127. * Tests that it is possible to set a custom validator under a name
  3128. */
  3129. public function testValidatorSetter(): void
  3130. {
  3131. $table = new Table();
  3132. $validator = new Validator();
  3133. $table->setValidator('other', $validator);
  3134. $this->assertSame($validator, $table->getValidator('other'));
  3135. $this->assertSame($table, $validator->getProvider('table'));
  3136. }
  3137. /**
  3138. * Tests hasValidator method.
  3139. */
  3140. public function testHasValidator(): void
  3141. {
  3142. $table = new Table();
  3143. $this->assertTrue($table->hasValidator('default'));
  3144. $this->assertFalse($table->hasValidator('other'));
  3145. $validator = new Validator();
  3146. $table->setValidator('other', $validator);
  3147. $this->assertTrue($table->hasValidator('other'));
  3148. }
  3149. /**
  3150. * Tests that the source of an existing Entity is the same as a new one
  3151. */
  3152. public function testEntitySourceExistingAndNew(): void
  3153. {
  3154. $this->loadPlugins(['TestPlugin']);
  3155. $table = $this->getTableLocator()->get('TestPlugin.Authors');
  3156. $existingAuthor = $table->find()->first();
  3157. $newAuthor = $table->newEmptyEntity();
  3158. $this->assertSame('TestPlugin.Authors', $existingAuthor->getSource());
  3159. $this->assertSame('TestPlugin.Authors', $newAuthor->getSource());
  3160. }
  3161. /**
  3162. * Tests that calling an entity with an empty array will run validation.
  3163. */
  3164. public function testNewEntityAndValidation(): void
  3165. {
  3166. $table = $this->getTableLocator()->get('Articles');
  3167. $table->getValidator()->requirePresence('title');
  3168. $entity = $table->newEntity([]);
  3169. $errors = $entity->getErrors();
  3170. $this->assertNotEmpty($errors['title']);
  3171. }
  3172. /**
  3173. * Tests that creating an entity will not run any validation.
  3174. */
  3175. public function testCreateEntityAndValidation(): void
  3176. {
  3177. $table = $this->getTableLocator()->get('Articles');
  3178. $table->getValidator()->requirePresence('title');
  3179. $entity = $table->newEmptyEntity();
  3180. $this->assertEmpty($entity->getErrors());
  3181. }
  3182. /**
  3183. * Test magic findByXX method.
  3184. */
  3185. public function testMagicFindDefaultToAll(): void
  3186. {
  3187. $table = $this->getTableLocator()->get('Users');
  3188. $result = $table->findByUsername('garrett');
  3189. $this->assertInstanceOf('Cake\ORM\Query', $result);
  3190. $expected = new QueryExpression(['Users.username' => 'garrett'], $this->usersTypeMap);
  3191. $this->assertEquals($expected, $result->clause('where'));
  3192. }
  3193. /**
  3194. * Test magic findByXX errors on missing arguments.
  3195. */
  3196. public function testMagicFindError(): void
  3197. {
  3198. $this->expectException(BadMethodCallException::class);
  3199. $this->expectExceptionMessage('Not enough arguments for magic finder. Got 0 required 1');
  3200. $table = $this->getTableLocator()->get('Users');
  3201. $table->findByUsername();
  3202. }
  3203. /**
  3204. * Test magic findByXX errors on missing arguments.
  3205. */
  3206. public function testMagicFindErrorMissingField(): void
  3207. {
  3208. $this->expectException(BadMethodCallException::class);
  3209. $this->expectExceptionMessage('Not enough arguments for magic finder. Got 1 required 2');
  3210. $table = $this->getTableLocator()->get('Users');
  3211. $table->findByUsernameAndId('garrett');
  3212. }
  3213. /**
  3214. * Test magic findByXX errors when there is a mix of or & and.
  3215. */
  3216. public function testMagicFindErrorMixOfOperators(): void
  3217. {
  3218. $this->expectException(BadMethodCallException::class);
  3219. $this->expectExceptionMessage('Cannot mix "and" & "or" in a magic finder. Use find() instead.');
  3220. $table = $this->getTableLocator()->get('Users');
  3221. $table->findByUsernameAndIdOrPassword('garrett', 1, 'sekret');
  3222. }
  3223. /**
  3224. * Test magic findByXX method.
  3225. */
  3226. public function testMagicFindFirstAnd(): void
  3227. {
  3228. $table = $this->getTableLocator()->get('Users');
  3229. $result = $table->findByUsernameAndId('garrett', 4);
  3230. $this->assertInstanceOf('Cake\ORM\Query', $result);
  3231. $expected = new QueryExpression(['Users.username' => 'garrett', 'Users.id' => 4], $this->usersTypeMap);
  3232. $this->assertEquals($expected, $result->clause('where'));
  3233. }
  3234. /**
  3235. * Test magic findByXX method.
  3236. */
  3237. public function testMagicFindFirstOr(): void
  3238. {
  3239. $table = $this->getTableLocator()->get('Users');
  3240. $result = $table->findByUsernameOrId('garrett', 4);
  3241. $this->assertInstanceOf('Cake\ORM\Query', $result);
  3242. $expected = new QueryExpression([], $this->usersTypeMap);
  3243. $expected->add(
  3244. [
  3245. 'OR' => [
  3246. 'Users.username' => 'garrett',
  3247. 'Users.id' => 4,
  3248. ],
  3249. ]
  3250. );
  3251. $this->assertEquals($expected, $result->clause('where'));
  3252. }
  3253. /**
  3254. * Test magic findAllByXX method.
  3255. */
  3256. public function testMagicFindAll(): void
  3257. {
  3258. $table = $this->getTableLocator()->get('Articles');
  3259. $result = $table->findAllByAuthorId(1);
  3260. $this->assertInstanceOf('Cake\ORM\Query', $result);
  3261. $this->assertNull($result->clause('limit'));
  3262. $expected = new QueryExpression(['Articles.author_id' => 1], $this->articlesTypeMap);
  3263. $this->assertEquals($expected, $result->clause('where'));
  3264. }
  3265. /**
  3266. * Test magic findAllByXX method.
  3267. */
  3268. public function testMagicFindAllAnd(): void
  3269. {
  3270. $table = $this->getTableLocator()->get('Users');
  3271. $result = $table->findAllByAuthorIdAndPublished(1, 'Y');
  3272. $this->assertInstanceOf('Cake\ORM\Query', $result);
  3273. $this->assertNull($result->clause('limit'));
  3274. $expected = new QueryExpression(
  3275. ['Users.author_id' => 1, 'Users.published' => 'Y'],
  3276. $this->usersTypeMap
  3277. );
  3278. $this->assertEquals($expected, $result->clause('where'));
  3279. }
  3280. /**
  3281. * Test magic findAllByXX method.
  3282. */
  3283. public function testMagicFindAllOr(): void
  3284. {
  3285. $table = $this->getTableLocator()->get('Users');
  3286. $result = $table->findAllByAuthorIdOrPublished(1, 'Y');
  3287. $this->assertInstanceOf('Cake\ORM\Query', $result);
  3288. $this->assertNull($result->clause('limit'));
  3289. $expected = new QueryExpression();
  3290. $expected->getTypeMap()->setDefaults($this->usersTypeMap->toArray());
  3291. $expected->add(
  3292. ['or' => ['Users.author_id' => 1, 'Users.published' => 'Y']]
  3293. );
  3294. $this->assertEquals($expected, $result->clause('where'));
  3295. $this->assertNull($result->clause('order'));
  3296. }
  3297. /**
  3298. * Test the behavior method.
  3299. */
  3300. public function testBehaviorIntrospection(): void
  3301. {
  3302. $table = $this->getTableLocator()->get('users');
  3303. $table->addBehavior('Timestamp');
  3304. $this->assertTrue($table->hasBehavior('Timestamp'), 'should be true on loaded behavior');
  3305. $this->assertFalse($table->hasBehavior('Tree'), 'should be false on unloaded behavior');
  3306. }
  3307. /**
  3308. * Tests saving belongsTo association
  3309. *
  3310. * @group save
  3311. */
  3312. public function testSaveBelongsTo(): void
  3313. {
  3314. $entity = new Entity([
  3315. 'title' => 'A Title',
  3316. 'body' => 'A body',
  3317. ]);
  3318. $entity->author = new Entity([
  3319. 'name' => 'Jose',
  3320. ]);
  3321. $table = $this->getTableLocator()->get('articles');
  3322. $table->belongsTo('authors');
  3323. $this->assertSame($entity, $table->save($entity));
  3324. $this->assertFalse($entity->isNew());
  3325. $this->assertFalse($entity->author->isNew());
  3326. $this->assertSame(5, $entity->author->id);
  3327. $this->assertSame(5, $entity->get('author_id'));
  3328. }
  3329. /**
  3330. * Tests saving hasOne association
  3331. *
  3332. * @group save
  3333. */
  3334. public function testSaveHasOne(): void
  3335. {
  3336. $entity = new Entity([
  3337. 'name' => 'Jose',
  3338. ]);
  3339. $entity->article = new Entity([
  3340. 'title' => 'A Title',
  3341. 'body' => 'A body',
  3342. ]);
  3343. $table = $this->getTableLocator()->get('authors');
  3344. $table->associations()->remove('Articles');
  3345. $table->hasOne('Articles');
  3346. $this->assertSame($entity, $table->save($entity));
  3347. $this->assertFalse($entity->isNew());
  3348. $this->assertFalse($entity->article->isNew());
  3349. $this->assertSame(4, $entity->article->id);
  3350. $this->assertSame(5, $entity->article->get('author_id'));
  3351. $this->assertFalse($entity->article->isDirty('author_id'));
  3352. }
  3353. /**
  3354. * Tests saving associations only saves associations
  3355. * if they are entities.
  3356. *
  3357. * @group save
  3358. */
  3359. public function testSaveOnlySaveAssociatedEntities(): void
  3360. {
  3361. $entity = new Entity([
  3362. 'name' => 'Jose',
  3363. ]);
  3364. // Not an entity.
  3365. $entity->article = [
  3366. 'title' => 'A Title',
  3367. 'body' => 'A body',
  3368. ];
  3369. $table = $this->getTableLocator()->get('authors');
  3370. // $table->hasOne('articles');
  3371. $table->save($entity);
  3372. $this->assertFalse($entity->isNew());
  3373. $this->assertIsArray($entity->article);
  3374. }
  3375. /**
  3376. * Tests saving multiple entities in a hasMany association
  3377. */
  3378. public function testSaveHasMany(): void
  3379. {
  3380. $entity = new Entity([
  3381. 'name' => 'Jose',
  3382. ]);
  3383. $entity->articles = [
  3384. new Entity([
  3385. 'title' => 'A Title',
  3386. 'body' => 'A body',
  3387. ]),
  3388. new Entity([
  3389. 'title' => 'Another Title',
  3390. 'body' => 'Another body',
  3391. ]),
  3392. ];
  3393. $table = $this->getTableLocator()->get('authors');
  3394. $this->assertSame($entity, $table->save($entity));
  3395. $this->assertFalse($entity->isNew());
  3396. $this->assertFalse($entity->articles[0]->isNew());
  3397. $this->assertFalse($entity->articles[1]->isNew());
  3398. $this->assertSame(4, $entity->articles[0]->id);
  3399. $this->assertSame(5, $entity->articles[1]->id);
  3400. $this->assertSame(5, $entity->articles[0]->author_id);
  3401. $this->assertSame(5, $entity->articles[1]->author_id);
  3402. }
  3403. /**
  3404. * Tests overwriting hasMany associations in an integration scenario.
  3405. */
  3406. public function testSaveHasManyOverwrite(): void
  3407. {
  3408. $table = $this->getTableLocator()->get('authors');
  3409. $entity = $table->get(3, contain: ['Articles']);
  3410. $data = [
  3411. 'name' => 'big jose',
  3412. 'articles' => [
  3413. [
  3414. 'id' => 2,
  3415. 'title' => 'New title',
  3416. ],
  3417. ],
  3418. ];
  3419. $entity = $table->patchEntity($entity, $data, ['associated' => 'Articles']);
  3420. $this->assertSame($entity, $table->save($entity));
  3421. $entity = $table->get(3, contain: ['Articles']);
  3422. $this->assertSame('big jose', $entity->name, 'Author did not persist');
  3423. $this->assertSame('New title', $entity->articles[0]->title, 'Article did not persist');
  3424. }
  3425. /**
  3426. * Tests saving belongsToMany records
  3427. *
  3428. * @group save
  3429. */
  3430. public function testSaveBelongsToMany(): void
  3431. {
  3432. $entity = new Entity([
  3433. 'title' => 'A Title',
  3434. 'body' => 'A body',
  3435. ]);
  3436. $entity->tags = [
  3437. new Entity([
  3438. 'name' => 'Something New',
  3439. ]),
  3440. new Entity([
  3441. 'name' => 'Another Something',
  3442. ]),
  3443. ];
  3444. $table = $this->getTableLocator()->get('Articles');
  3445. $this->assertSame($entity, $table->save($entity));
  3446. $this->assertFalse($entity->isNew());
  3447. $this->assertFalse($entity->tags[0]->isNew());
  3448. $this->assertFalse($entity->tags[1]->isNew());
  3449. $this->assertSame(4, $entity->tags[0]->id);
  3450. $this->assertSame(5, $entity->tags[1]->id);
  3451. $this->assertSame(4, $entity->tags[0]->_joinData->article_id);
  3452. $this->assertSame(4, $entity->tags[1]->_joinData->article_id);
  3453. $this->assertSame(4, $entity->tags[0]->_joinData->tag_id);
  3454. $this->assertSame(5, $entity->tags[1]->_joinData->tag_id);
  3455. }
  3456. /**
  3457. * Tests saving belongsToMany records when record exists.
  3458. *
  3459. * @group save
  3460. */
  3461. public function testSaveBelongsToManyJoinDataOnExistingRecord(): void
  3462. {
  3463. $tags = $this->getTableLocator()->get('Tags');
  3464. $table = $this->getTableLocator()->get('Articles');
  3465. $entity = $table->find()->contain('Tags')->first();
  3466. // not associated to the article already.
  3467. $entity->tags[] = $tags->get(3);
  3468. $entity->setDirty('tags', true);
  3469. $this->assertSame($entity, $table->save($entity));
  3470. $this->assertFalse($entity->isNew());
  3471. $this->assertFalse($entity->tags[0]->isNew());
  3472. $this->assertFalse($entity->tags[1]->isNew());
  3473. $this->assertFalse($entity->tags[2]->isNew());
  3474. $this->assertNotEmpty($entity->tags[0]->_joinData);
  3475. $this->assertNotEmpty($entity->tags[1]->_joinData);
  3476. $this->assertNotEmpty($entity->tags[2]->_joinData);
  3477. }
  3478. /**
  3479. * Test that belongsToMany can be saved with _joinData data.
  3480. */
  3481. public function testSaveBelongsToManyJoinData(): void
  3482. {
  3483. $articles = $this->getTableLocator()->get('Articles');
  3484. $article = $articles->get(1, contain: ['Tags']);
  3485. $data = [
  3486. 'tags' => [
  3487. ['id' => 1, '_joinData' => ['highlighted' => 1]],
  3488. ['id' => 3],
  3489. ],
  3490. ];
  3491. $article = $articles->patchEntity($article, $data);
  3492. $result = $articles->save($article);
  3493. $this->assertSame($result, $article);
  3494. }
  3495. /**
  3496. * Test to check that association condition are used when fetching existing
  3497. * records to decide which records to unlink.
  3498. */
  3499. public function testPolymorphicBelongsToManySave(): void
  3500. {
  3501. $articles = $this->getTableLocator()->get('Articles');
  3502. $articles->Tags->setThrough('PolymorphicTagged')
  3503. ->setForeignKey('foreign_key')
  3504. ->setConditions(['PolymorphicTagged.foreign_model' => 'Articles'])
  3505. ->setSort(['PolymorphicTagged.position' => 'ASC']);
  3506. $entity = $articles->get(1, contain: ['Tags']);
  3507. $data = [
  3508. 'id' => 1,
  3509. 'tags' => [
  3510. [
  3511. 'id' => 1,
  3512. '_joinData' => [
  3513. 'id' => 2,
  3514. 'foreign_model' => 'Articles',
  3515. 'position' => 2,
  3516. ],
  3517. ],
  3518. [
  3519. 'id' => 2,
  3520. '_joinData' => [
  3521. 'foreign_model' => 'Articles',
  3522. 'position' => 1,
  3523. ],
  3524. ],
  3525. ],
  3526. ];
  3527. $entity = $articles->patchEntity($entity, $data, ['associated' => ['Tags._joinData']]);
  3528. $entity = $articles->save($entity);
  3529. $expected = [
  3530. [
  3531. 'id' => 1,
  3532. 'tag_id' => 1,
  3533. 'foreign_key' => 1,
  3534. 'foreign_model' => 'Posts',
  3535. 'position' => 1,
  3536. ],
  3537. [
  3538. 'id' => 2,
  3539. 'tag_id' => 1,
  3540. 'foreign_key' => 1,
  3541. 'foreign_model' => 'Articles',
  3542. 'position' => 2,
  3543. ],
  3544. [
  3545. 'id' => 3,
  3546. 'tag_id' => 2,
  3547. 'foreign_key' => 1,
  3548. 'foreign_model' => 'Articles',
  3549. 'position' => 1,
  3550. ],
  3551. ];
  3552. $result = $this->getTableLocator()->get('PolymorphicTagged')
  3553. ->find('all', sort: ['id' => 'DESC'])
  3554. ->enableHydration(false)
  3555. ->toArray();
  3556. $this->assertEquals($expected, $result);
  3557. }
  3558. /**
  3559. * Tests saving belongsToMany records can delete all links.
  3560. *
  3561. * @group save
  3562. */
  3563. public function testSaveBelongsToManyDeleteAllLinks(): void
  3564. {
  3565. $table = $this->getTableLocator()->get('Articles');
  3566. $table->Tags->setSaveStrategy('replace');
  3567. $entity = $table->get(1, contain: 'Tags');
  3568. $this->assertCount(2, $entity->tags, 'Fixture data did not change.');
  3569. $entity->tags = [];
  3570. $result = $table->save($entity);
  3571. $this->assertSame($result, $entity);
  3572. $this->assertSame([], $entity->tags, 'No tags on the entity.');
  3573. $entity = $table->get(1, contain: 'Tags');
  3574. $this->assertSame([], $entity->tags, 'No tags in the db either.');
  3575. }
  3576. /**
  3577. * Tests saving belongsToMany records can delete some links.
  3578. *
  3579. * @group save
  3580. */
  3581. public function testSaveBelongsToManyDeleteSomeLinks(): void
  3582. {
  3583. $table = $this->getTableLocator()->get('Articles');
  3584. $table->Tags->setSaveStrategy('replace');
  3585. $entity = $table->get(1, contain: 'Tags');
  3586. $this->assertCount(2, $entity->tags, 'Fixture data did not change.');
  3587. $tag = new Entity([
  3588. 'id' => 2,
  3589. ]);
  3590. $entity->tags = [$tag];
  3591. $result = $table->save($entity);
  3592. $this->assertSame($result, $entity);
  3593. $this->assertCount(1, $entity->tags, 'Only one tag left.');
  3594. $this->assertEquals($tag, $entity->tags[0]);
  3595. $entity = $table->get(1, contain: 'Tags');
  3596. $this->assertCount(1, $entity->tags, 'Only one tag in the db.');
  3597. $this->assertEquals($tag->id, $entity->tags[0]->id);
  3598. }
  3599. /**
  3600. * Test that belongsToMany ignores non-entity data.
  3601. */
  3602. public function testSaveBelongsToManyIgnoreNonEntityData(): void
  3603. {
  3604. $articles = $this->getTableLocator()->get('Articles');
  3605. $article = $articles->get(1, contain: ['Tags']);
  3606. $article->tags = [
  3607. '_ids' => [2, 1],
  3608. ];
  3609. $result = $articles->save($article);
  3610. $this->assertSame($result, $article);
  3611. }
  3612. /**
  3613. * Tests that saving a persisted and clean entity will is a no-op
  3614. *
  3615. * @group save
  3616. */
  3617. public function testSaveCleanEntity(): void
  3618. {
  3619. $table = $this->getMockBuilder(Table::class)
  3620. ->onlyMethods(['_processSave'])
  3621. ->getMock();
  3622. $entity = new Entity(
  3623. ['id' => 'foo'],
  3624. ['markNew' => false, 'markClean' => true]
  3625. );
  3626. $table->expects($this->never())->method('_processSave');
  3627. $this->assertSame($entity, $table->save($entity));
  3628. }
  3629. /**
  3630. * Integration test to show how to append a new tag to an article
  3631. *
  3632. * @group save
  3633. */
  3634. public function testBelongsToManyIntegration(): void
  3635. {
  3636. $table = $this->getTableLocator()->get('Articles');
  3637. $article = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  3638. $tags = $article->tags;
  3639. $this->assertNotEmpty($tags);
  3640. $tags[] = new Tag(['name' => 'Something New']);
  3641. $article->tags = $tags;
  3642. $this->assertSame($article, $table->save($article));
  3643. $tags = $article->tags;
  3644. $this->assertCount(3, $tags);
  3645. $this->assertFalse($tags[2]->isNew());
  3646. $this->assertSame(4, $tags[2]->id);
  3647. $this->assertSame(1, $tags[2]->_joinData->article_id);
  3648. $this->assertSame(4, $tags[2]->_joinData->tag_id);
  3649. }
  3650. /**
  3651. * Tests that it is possible to do a deep save and control what associations get saved,
  3652. * while having control of the options passed to each level of the save
  3653. *
  3654. * @group save
  3655. */
  3656. public function testSaveDeepAssociationOptions(): void
  3657. {
  3658. $articles = $this->getMockBuilder(Table::class)
  3659. ->onlyMethods(['_insert'])
  3660. ->setConstructorArgs([['table' => 'articles', 'connection' => $this->connection]])
  3661. ->getMock();
  3662. $authors = $this->getMockBuilder(Table::class)
  3663. ->onlyMethods(['_insert'])
  3664. ->setConstructorArgs([['table' => 'authors', 'connection' => $this->connection]])
  3665. ->getMock();
  3666. $supervisors = $this->getMockBuilder(Table::class)
  3667. ->onlyMethods(['_insert'])
  3668. ->setConstructorArgs([[
  3669. 'table' => 'authors',
  3670. 'alias' => 'supervisors',
  3671. 'connection' => $this->connection,
  3672. ]])
  3673. ->getMock();
  3674. $tags = $this->getMockBuilder(Table::class)
  3675. ->onlyMethods(['_insert'])
  3676. ->setConstructorArgs([['table' => 'tags', 'connection' => $this->connection]])
  3677. ->getMock();
  3678. $articles->belongsTo('authors', ['targetTable' => $authors]);
  3679. $authors->hasOne('supervisors', ['targetTable' => $supervisors]);
  3680. $supervisors->belongsToMany('tags', ['targetTable' => $tags]);
  3681. $entity = new Entity([
  3682. 'title' => 'bar',
  3683. 'author' => new Entity([
  3684. 'name' => 'Juan',
  3685. 'supervisor' => new Entity(['name' => 'Marc']),
  3686. 'tags' => [
  3687. new Entity(['name' => 'foo']),
  3688. ],
  3689. ]),
  3690. ]);
  3691. $entity->setNew(true);
  3692. $entity->author->setNew(true);
  3693. $entity->author->supervisor->setNew(true);
  3694. $entity->author->tags[0]->setNew(true);
  3695. $articles->expects($this->once())
  3696. ->method('_insert')
  3697. ->with($entity, ['title' => 'bar'])
  3698. ->willReturn($entity);
  3699. $authors->expects($this->once())
  3700. ->method('_insert')
  3701. ->with($entity->author, ['name' => 'Juan'])
  3702. ->willReturn($entity->author);
  3703. $supervisors->expects($this->once())
  3704. ->method('_insert')
  3705. ->with($entity->author->supervisor, ['name' => 'Marc'])
  3706. ->willReturn($entity->author->supervisor);
  3707. $tags->expects($this->never())->method('_insert');
  3708. $this->assertSame($entity, $articles->save($entity, [
  3709. 'associated' => [
  3710. 'authors' => [],
  3711. 'authors.supervisors' => [
  3712. 'atomic' => false,
  3713. 'associated' => false,
  3714. ],
  3715. ],
  3716. ]));
  3717. }
  3718. public function testBelongsToFluentInterface(): void
  3719. {
  3720. /** @var \TestApp\Model\Table\ArticlesTable $articles */
  3721. $articles = $this->getMockBuilder(Table::class)
  3722. ->onlyMethods(['_insert'])
  3723. ->setConstructorArgs([['table' => 'articles', 'connection' => $this->connection]])
  3724. ->getMock();
  3725. $authors = $this->getMockBuilder(Table::class)
  3726. ->onlyMethods(['_insert'])
  3727. ->setConstructorArgs([['table' => 'authors', 'connection' => $this->connection]])
  3728. ->getMock();
  3729. try {
  3730. $articles->belongsTo('Articles')
  3731. ->setForeignKey('author_id')
  3732. ->setTarget($authors)
  3733. ->setBindingKey('id')
  3734. ->setConditions([])
  3735. ->setFinder('list')
  3736. ->setProperty('authors')
  3737. ->setJoinType('inner');
  3738. } catch (BadMethodCallException $e) {
  3739. $this->fail('Method chaining should be ok');
  3740. }
  3741. $this->assertSame('articles', $articles->getTable());
  3742. }
  3743. public function testHasOneFluentInterface(): void
  3744. {
  3745. /** @var \TestApp\Model\Table\AuthorsTable $authors */
  3746. $authors = $this->getMockBuilder(Table::class)
  3747. ->onlyMethods(['_insert'])
  3748. ->setConstructorArgs([['table' => 'authors', 'connection' => $this->connection]])
  3749. ->getMock();
  3750. try {
  3751. $authors->hasOne('Articles')
  3752. ->setForeignKey('author_id')
  3753. ->setDependent(true)
  3754. ->setBindingKey('id')
  3755. ->setConditions([])
  3756. ->setCascadeCallbacks(true)
  3757. ->setFinder('list')
  3758. ->setStrategy('select')
  3759. ->setProperty('authors')
  3760. ->setJoinType('inner');
  3761. } catch (BadMethodCallException $e) {
  3762. $this->fail('Method chaining should be ok');
  3763. }
  3764. $this->assertSame('authors', $authors->getTable());
  3765. }
  3766. public function testHasManyFluentInterface(): void
  3767. {
  3768. /** @var \TestApp\Model\Table\AuthorsTable $authors */
  3769. $authors = $this->getMockBuilder(Table::class)
  3770. ->onlyMethods(['_insert'])
  3771. ->setConstructorArgs([['table' => 'authors', 'connection' => $this->connection]])
  3772. ->getMock();
  3773. try {
  3774. $authors->hasMany('Articles')
  3775. ->setForeignKey('author_id')
  3776. ->setDependent(true)
  3777. ->setSort(['created' => 'DESC'])
  3778. ->setBindingKey('id')
  3779. ->setConditions([])
  3780. ->setCascadeCallbacks(true)
  3781. ->setFinder('list')
  3782. ->setStrategy('select')
  3783. ->setSaveStrategy('replace')
  3784. ->setProperty('authors')
  3785. ->setJoinType('inner');
  3786. } catch (BadMethodCallException $e) {
  3787. $this->fail('Method chaining should be ok');
  3788. }
  3789. $this->assertSame('authors', $authors->getTable());
  3790. }
  3791. public function testBelongsToManyFluentInterface(): void
  3792. {
  3793. /** @var \TestApp\Model\Table\AuthorsTable $authors */
  3794. $authors = $this->getMockBuilder(Table::class)
  3795. ->onlyMethods(['_insert'])
  3796. ->setConstructorArgs([['table' => 'authors', 'connection' => $this->connection]])
  3797. ->getMock();
  3798. try {
  3799. $authors->belongsToMany('Articles')
  3800. ->setForeignKey('author_id')
  3801. ->setDependent(true)
  3802. ->setTargetForeignKey('article_id')
  3803. ->setBindingKey('id')
  3804. ->setConditions([])
  3805. ->setFinder('list')
  3806. ->setProperty('authors')
  3807. ->setSource($authors)
  3808. ->setStrategy('select')
  3809. ->setSaveStrategy('append')
  3810. ->setThrough('author_articles')
  3811. ->setJoinType('inner');
  3812. } catch (BadMethodCallException $e) {
  3813. $this->fail('Method chaining should be ok');
  3814. }
  3815. $this->assertSame('authors', $authors->getTable());
  3816. }
  3817. /**
  3818. * Integration test for linking entities with belongsToMany
  3819. */
  3820. public function testLinkBelongsToMany(): void
  3821. {
  3822. $table = $this->getTableLocator()->get('Articles');
  3823. $tagsTable = $this->getTableLocator()->get('Tags');
  3824. $source = ['source' => 'Tags'];
  3825. $options = ['markNew' => false];
  3826. $article = new Entity([
  3827. 'id' => 1,
  3828. ], $options);
  3829. $newTag = new Tag([
  3830. 'name' => 'Foo',
  3831. 'description' => 'Foo desc',
  3832. 'created' => null,
  3833. ], $source);
  3834. $tags[] = new Tag([
  3835. 'id' => 3,
  3836. ], $options + $source);
  3837. $tags[] = $newTag;
  3838. $tagsTable->save($newTag);
  3839. $table->getAssociation('Tags')->link($article, $tags);
  3840. $this->assertEquals($article->tags, $tags);
  3841. foreach ($tags as $tag) {
  3842. $this->assertFalse($tag->isNew());
  3843. }
  3844. $article = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  3845. $this->assertEquals($article->tags[2]->id, $tags[0]->id);
  3846. $this->assertEqualsCanonicalizing($article->tags[3], $tags[1]);
  3847. }
  3848. /**
  3849. * Integration test for linking entities with HasMany
  3850. */
  3851. public function testLinkHasMany(): void
  3852. {
  3853. $authors = $this->getTableLocator()->get('Authors');
  3854. $articles = $this->getTableLocator()->get('Articles');
  3855. $author = $authors->newEntity(['name' => 'mylux']);
  3856. $author = $authors->save($author);
  3857. $newArticles = $articles->newEntities(
  3858. [
  3859. [
  3860. 'title' => 'New bakery next corner',
  3861. 'body' => 'They sell tastefull cakes',
  3862. ],
  3863. [
  3864. 'title' => 'Spicy cake recipe',
  3865. 'body' => 'chocolate and peppers',
  3866. ],
  3867. ]
  3868. );
  3869. $sizeArticles = count($newArticles);
  3870. $this->assertTrue($authors->Articles->link($author, $newArticles));
  3871. $this->assertCount($sizeArticles, $authors->Articles->findAllByAuthorId($author->id));
  3872. $this->assertCount($sizeArticles, $author->articles);
  3873. $this->assertFalse($author->isDirty('articles'));
  3874. }
  3875. /**
  3876. * Integration test for linking entities with HasMany combined with ReplaceSaveStrategy. It must append, not unlinking anything
  3877. */
  3878. public function testLinkHasManyReplaceSaveStrategy(): void
  3879. {
  3880. $authors = $this->getTableLocator()->get('Authors');
  3881. $articles = $this->getTableLocator()->get('Articles');
  3882. $authors->Articles->setSaveStrategy('replace');
  3883. $author = $authors->newEntity(['name' => 'mylux']);
  3884. $author = $authors->save($author);
  3885. $newArticles = $articles->newEntities(
  3886. [
  3887. [
  3888. 'title' => 'New bakery next corner',
  3889. 'body' => 'They sell tastefull cakes',
  3890. ],
  3891. [
  3892. 'title' => 'Spicy cake recipe',
  3893. 'body' => 'chocolate and peppers',
  3894. ],
  3895. ]
  3896. );
  3897. $this->assertTrue($authors->Articles->link($author, $newArticles));
  3898. $sizeArticles = count($newArticles);
  3899. $newArticles = $articles->newEntities(
  3900. [
  3901. [
  3902. 'title' => 'Nothing but the cake',
  3903. 'body' => 'It is all that we need',
  3904. ],
  3905. ]
  3906. );
  3907. $this->assertTrue($authors->Articles->link($author, $newArticles));
  3908. $sizeArticles++;
  3909. $this->assertCount($sizeArticles, $authors->Articles->findAllByAuthorId($author->id));
  3910. $this->assertCount($sizeArticles, $author->articles);
  3911. $this->assertFalse($author->isDirty('articles'));
  3912. }
  3913. /**
  3914. * Integration test for linking entities with HasMany. The input contains already linked entities and they should not appeat duplicated
  3915. */
  3916. public function testLinkHasManyExisting(): void
  3917. {
  3918. $authors = $this->getTableLocator()->get('Authors');
  3919. $articles = $this->getTableLocator()->get('Articles');
  3920. $authors->Articles->setSaveStrategy('replace');
  3921. $author = $authors->newEntity(['name' => 'mylux']);
  3922. $author = $authors->save($author);
  3923. $newArticles = $articles->newEntities(
  3924. [
  3925. [
  3926. 'title' => 'New bakery next corner',
  3927. 'body' => 'They sell tastefull cakes',
  3928. ],
  3929. [
  3930. 'title' => 'Spicy cake recipe',
  3931. 'body' => 'chocolate and peppers',
  3932. ],
  3933. ]
  3934. );
  3935. $this->assertTrue($authors->Articles->link($author, $newArticles));
  3936. $sizeArticles = count($newArticles);
  3937. $newArticles = array_merge(
  3938. $author->articles,
  3939. $articles->newEntities(
  3940. [
  3941. [
  3942. 'title' => 'Nothing but the cake',
  3943. 'body' => 'It is all that we need',
  3944. ],
  3945. ]
  3946. )
  3947. );
  3948. $this->assertTrue($authors->Articles->link($author, $newArticles));
  3949. $sizeArticles++;
  3950. $this->assertCount($sizeArticles, $authors->Articles->findAllByAuthorId($author->id));
  3951. $this->assertCount($sizeArticles, $author->articles);
  3952. $this->assertFalse($author->isDirty('articles'));
  3953. }
  3954. /**
  3955. * Integration test for unlinking entities with HasMany. The association property must be cleaned
  3956. */
  3957. public function testUnlinkHasManyCleanProperty(): void
  3958. {
  3959. $authors = $this->getTableLocator()->get('Authors');
  3960. $articles = $this->getTableLocator()->get('Articles');
  3961. $authors->Articles->setSaveStrategy('replace');
  3962. $author = $authors->newEntity(['name' => 'mylux']);
  3963. $author = $authors->save($author);
  3964. $newArticles = $articles->newEntities(
  3965. [
  3966. [
  3967. 'title' => 'New bakery next corner',
  3968. 'body' => 'They sell tastefull cakes',
  3969. ],
  3970. [
  3971. 'title' => 'Spicy cake recipe',
  3972. 'body' => 'chocolate and peppers',
  3973. ],
  3974. [
  3975. 'title' => 'Creamy cake recipe',
  3976. 'body' => 'chocolate and cream',
  3977. ],
  3978. ]
  3979. );
  3980. $this->assertTrue($authors->Articles->link($author, $newArticles));
  3981. $sizeArticles = count($newArticles);
  3982. $articlesToUnlink = [$author->articles[0], $author->articles[1]];
  3983. $authors->Articles->unlink($author, $articlesToUnlink);
  3984. $this->assertCount($sizeArticles - count($articlesToUnlink), $authors->Articles->findAllByAuthorId($author->id));
  3985. $this->assertCount($sizeArticles - count($articlesToUnlink), $author->articles);
  3986. $this->assertFalse($author->isDirty('articles'));
  3987. }
  3988. /**
  3989. * Integration test for unlinking entities with HasMany. The association property must stay unchanged
  3990. */
  3991. public function testUnlinkHasManyNotCleanProperty(): void
  3992. {
  3993. $authors = $this->getTableLocator()->get('Authors');
  3994. $articles = $this->getTableLocator()->get('Articles');
  3995. $authors->Articles->setSaveStrategy('replace');
  3996. $author = $authors->newEntity(['name' => 'mylux']);
  3997. $author = $authors->save($author);
  3998. $newArticles = $articles->newEntities(
  3999. [
  4000. [
  4001. 'title' => 'New bakery next corner',
  4002. 'body' => 'They sell tastefull cakes',
  4003. ],
  4004. [
  4005. 'title' => 'Spicy cake recipe',
  4006. 'body' => 'chocolate and peppers',
  4007. ],
  4008. [
  4009. 'title' => 'Creamy cake recipe',
  4010. 'body' => 'chocolate and cream',
  4011. ],
  4012. ]
  4013. );
  4014. $this->assertTrue($authors->Articles->link($author, $newArticles));
  4015. $sizeArticles = count($newArticles);
  4016. $articlesToUnlink = [$author->articles[0], $author->articles[1]];
  4017. $authors->Articles->unlink($author, $articlesToUnlink, ['cleanProperty' => false]);
  4018. $this->assertCount($sizeArticles - count($articlesToUnlink), $authors->Articles->findAllByAuthorId($author->id));
  4019. $this->assertCount($sizeArticles, $author->articles);
  4020. $this->assertFalse($author->isDirty('articles'));
  4021. }
  4022. /**
  4023. * Integration test for unlinking entities with HasMany.
  4024. * Checking that no error happens when the hasMany property is originally
  4025. * null
  4026. */
  4027. public function testUnlinkHasManyEmpty(): void
  4028. {
  4029. $authors = $this->getTableLocator()->get('Authors');
  4030. $author = $authors->get(1);
  4031. $article = $authors->Articles->get(1);
  4032. $authors->Articles->unlink($author, [$article]);
  4033. $this->assertNotEmpty($authors);
  4034. }
  4035. /**
  4036. * Integration test for replacing entities which depend on their source entity with HasMany and failing transaction. False should be returned when
  4037. * unlinking fails while replacing even when cascadeCallbacks is enabled
  4038. */
  4039. public function testReplaceHasManyOnErrorDependentCascadeCallbacks(): void
  4040. {
  4041. $articles = $this->getMockBuilder(Table::class)
  4042. ->onlyMethods(['delete'])
  4043. ->setConstructorArgs([[
  4044. 'connection' => $this->connection,
  4045. 'alias' => 'Articles',
  4046. 'table' => 'articles',
  4047. ]])
  4048. ->getMock();
  4049. $articles->method('delete')->willReturn(false);
  4050. $associations = new AssociationCollection();
  4051. $hasManyArticles = $this->getMockBuilder('Cake\ORM\Association\HasMany')
  4052. ->onlyMethods(['getTarget'])
  4053. ->setConstructorArgs([
  4054. 'articles',
  4055. [
  4056. 'target' => $articles,
  4057. 'foreignKey' => 'author_id',
  4058. 'dependent' => true,
  4059. 'cascadeCallbacks' => true,
  4060. ],
  4061. ])
  4062. ->getMock();
  4063. $hasManyArticles->method('getTarget')->willReturn($articles);
  4064. $associations->add('Articles', $hasManyArticles);
  4065. $authors = new Table([
  4066. 'connection' => $this->connection,
  4067. 'alias' => 'Authors',
  4068. 'table' => 'authors',
  4069. 'associations' => $associations,
  4070. ]);
  4071. $authors->Articles->setSource($authors);
  4072. $author = $authors->newEntity(['name' => 'mylux']);
  4073. $author = $authors->save($author);
  4074. $newArticles = $articles->newEntities(
  4075. [
  4076. [
  4077. 'title' => 'New bakery next corner',
  4078. 'body' => 'They sell tastefull cakes',
  4079. ],
  4080. [
  4081. 'title' => 'Spicy cake recipe',
  4082. 'body' => 'chocolate and peppers',
  4083. ],
  4084. ]
  4085. );
  4086. $sizeArticles = count($newArticles);
  4087. $this->assertTrue($authors->Articles->link($author, $newArticles));
  4088. $this->assertEquals($authors->Articles->findAllByAuthorId($author->id)->count(), $sizeArticles);
  4089. $this->assertCount($sizeArticles, $author->articles);
  4090. $newArticles = array_merge(
  4091. $author->articles,
  4092. $articles->newEntities(
  4093. [
  4094. [
  4095. 'title' => 'Cheese cake recipe',
  4096. 'body' => 'The secrets of mixing salt and sugar',
  4097. ],
  4098. [
  4099. 'title' => 'Not another piece of cake',
  4100. 'body' => 'This is the best',
  4101. ],
  4102. ]
  4103. )
  4104. );
  4105. unset($newArticles[0]);
  4106. $this->assertFalse($authors->Articles->replace($author, $newArticles));
  4107. $this->assertCount($sizeArticles, $authors->Articles->findAllByAuthorId($author->id));
  4108. }
  4109. /**
  4110. * Integration test for replacing entities with HasMany and an empty target list. The transaction must be successful
  4111. */
  4112. public function testReplaceHasManyEmptyList(): void
  4113. {
  4114. $authors = new Table([
  4115. 'connection' => $this->connection,
  4116. 'alias' => 'Authors',
  4117. 'table' => 'authors',
  4118. ]);
  4119. $authors->hasMany('Articles');
  4120. $author = $authors->newEntity(['name' => 'mylux']);
  4121. $author = $authors->save($author);
  4122. $newArticles = $authors->Articles->newEntities(
  4123. [
  4124. [
  4125. 'title' => 'New bakery next corner',
  4126. 'body' => 'They sell tastefull cakes',
  4127. ],
  4128. [
  4129. 'title' => 'Spicy cake recipe',
  4130. 'body' => 'chocolate and peppers',
  4131. ],
  4132. ]
  4133. );
  4134. $sizeArticles = count($newArticles);
  4135. $this->assertTrue($authors->Articles->link($author, $newArticles));
  4136. $this->assertEquals($authors->Articles->findAllByAuthorId($author->id)->count(), $sizeArticles);
  4137. $this->assertCount($sizeArticles, $author->articles);
  4138. $newArticles = [];
  4139. $this->assertTrue($authors->Articles->replace($author, $newArticles));
  4140. $this->assertCount(0, $authors->Articles->findAllByAuthorId($author->id));
  4141. }
  4142. /**
  4143. * Integration test for replacing entities with HasMany and no already persisted entities. The transaction must be successful.
  4144. * Replace operation should prevent considering 0 changed records an error when they are not found in the table
  4145. */
  4146. public function testReplaceHasManyNoPersistedEntities(): void
  4147. {
  4148. $authors = new Table([
  4149. 'connection' => $this->connection,
  4150. 'alias' => 'Authors',
  4151. 'table' => 'authors',
  4152. ]);
  4153. $authors->hasMany('Articles');
  4154. $author = $authors->newEntity(['name' => 'mylux']);
  4155. $author = $authors->save($author);
  4156. $newArticles = $authors->Articles->newEntities(
  4157. [
  4158. [
  4159. 'title' => 'New bakery next corner',
  4160. 'body' => 'They sell tastefull cakes',
  4161. ],
  4162. [
  4163. 'title' => 'Spicy cake recipe',
  4164. 'body' => 'chocolate and peppers',
  4165. ],
  4166. ]
  4167. );
  4168. $authors->Articles->deleteAll(['1=1']);
  4169. $sizeArticles = count($newArticles);
  4170. $this->assertTrue($authors->Articles->link($author, $newArticles));
  4171. $this->assertEquals($authors->Articles->findAllByAuthorId($author->id)->count(), $sizeArticles);
  4172. $this->assertCount($sizeArticles, $author->articles);
  4173. $this->assertTrue($authors->Articles->replace($author, $newArticles));
  4174. $this->assertCount($sizeArticles, $authors->Articles->findAllByAuthorId($author->id));
  4175. }
  4176. /**
  4177. * Integration test for replacing entities with HasMany.
  4178. */
  4179. public function testReplaceHasMany(): void
  4180. {
  4181. $authors = $this->getTableLocator()->get('Authors');
  4182. $articles = $this->getTableLocator()->get('Articles');
  4183. $author = $authors->newEntity(['name' => 'mylux']);
  4184. $author = $authors->save($author);
  4185. $newArticles = $articles->newEntities(
  4186. [
  4187. [
  4188. 'title' => 'New bakery next corner',
  4189. 'body' => 'They sell tastefull cakes',
  4190. ],
  4191. [
  4192. 'title' => 'Spicy cake recipe',
  4193. 'body' => 'chocolate and peppers',
  4194. ],
  4195. ]
  4196. );
  4197. $sizeArticles = count($newArticles);
  4198. $this->assertTrue($authors->Articles->link($author, $newArticles));
  4199. $this->assertEquals($authors->Articles->findAllByAuthorId($author->id)->count(), $sizeArticles);
  4200. $this->assertCount($sizeArticles, $author->articles);
  4201. $newArticles = array_merge(
  4202. $author->articles,
  4203. $articles->newEntities(
  4204. [
  4205. [
  4206. 'title' => 'Cheese cake recipe',
  4207. 'body' => 'The secrets of mixing salt and sugar',
  4208. ],
  4209. [
  4210. 'title' => 'Not another piece of cake',
  4211. 'body' => 'This is the best',
  4212. ],
  4213. ]
  4214. )
  4215. );
  4216. unset($newArticles[0]);
  4217. $this->assertTrue($authors->Articles->replace($author, $newArticles));
  4218. $this->assertCount(count($newArticles), $author->articles);
  4219. $this->assertEquals((new Collection($newArticles))->extract('title'), (new Collection($author->articles))->extract('title'));
  4220. }
  4221. /**
  4222. * Integration test to show how to unlink a single record from a belongsToMany
  4223. */
  4224. public function testUnlinkBelongsToMany(): void
  4225. {
  4226. $table = $this->getTableLocator()->get('Articles');
  4227. $article = $table->find('all')
  4228. ->where(['id' => 1])
  4229. ->contain(['Tags'])->first();
  4230. $table->getAssociation('Tags')->unlink($article, [$article->tags[0]]);
  4231. $this->assertCount(1, $article->tags);
  4232. $this->assertSame(2, $article->tags[0]->get('id'));
  4233. $this->assertFalse($article->isDirty('tags'));
  4234. }
  4235. /**
  4236. * Integration test to show how to unlink multiple records from a belongsToMany
  4237. */
  4238. public function testUnlinkBelongsToManyMultiple(): void
  4239. {
  4240. $table = $this->getTableLocator()->get('Articles');
  4241. $options = ['markNew' => false];
  4242. $article = new Entity(['id' => 1], $options);
  4243. $tags[] = new Tag(['id' => 1], $options);
  4244. $tags[] = new Tag(['id' => 2], $options);
  4245. $table->getAssociation('Tags')->unlink($article, $tags);
  4246. $left = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  4247. $this->assertEmpty($left->tags);
  4248. }
  4249. /**
  4250. * Integration test to show how to unlink multiple records from a belongsToMany
  4251. * providing some of the joint
  4252. */
  4253. public function testUnlinkBelongsToManyPassingJoint(): void
  4254. {
  4255. $table = $this->getTableLocator()->get('Articles');
  4256. $options = ['markNew' => false];
  4257. $article = new Entity(['id' => 1], $options);
  4258. $tags[] = new Tag(['id' => 1], $options);
  4259. $tags[] = new Tag(['id' => 2], $options);
  4260. $tags[1]->_joinData = new Entity([
  4261. 'article_id' => 1,
  4262. 'tag_id' => 2,
  4263. ], $options);
  4264. $table->getAssociation('Tags')->unlink($article, $tags);
  4265. $left = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  4266. $this->assertEmpty($left->tags);
  4267. }
  4268. /**
  4269. * Integration test to show how to replace records from a belongsToMany
  4270. */
  4271. public function testReplacelinksBelongsToMany(): void
  4272. {
  4273. $table = $this->getTableLocator()->get('Articles');
  4274. $options = ['markNew' => false];
  4275. $article = new Entity(['id' => 1], $options);
  4276. $tags[] = new Tag(['id' => 2], $options);
  4277. $tags[] = new Tag(['id' => 3], $options);
  4278. $tags[] = new Tag(['name' => 'foo']);
  4279. $table->getAssociation('Tags')->replaceLinks($article, $tags);
  4280. $this->assertSame(2, $article->tags[0]->id);
  4281. $this->assertSame(3, $article->tags[1]->id);
  4282. $this->assertSame(4, $article->tags[2]->id);
  4283. $article = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  4284. $this->assertCount(3, $article->tags);
  4285. $this->assertSame(2, $article->tags[0]->id);
  4286. $this->assertSame(3, $article->tags[1]->id);
  4287. $this->assertSame(4, $article->tags[2]->id);
  4288. $this->assertSame('foo', $article->tags[2]->name);
  4289. }
  4290. /**
  4291. * Integration test to show how remove all links from a belongsToMany
  4292. */
  4293. public function testReplacelinksBelongsToManyWithEmpty(): void
  4294. {
  4295. $table = $this->getTableLocator()->get('Articles');
  4296. $options = ['markNew' => false];
  4297. $article = new Entity(['id' => 1], $options);
  4298. $tags = [];
  4299. $table->getAssociation('Tags')->replaceLinks($article, $tags);
  4300. $this->assertSame($tags, $article->tags);
  4301. $article = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  4302. $this->assertEmpty($article->tags);
  4303. }
  4304. /**
  4305. * Integration test to show how to replace records from a belongsToMany
  4306. * passing the joint property along in the target entity
  4307. */
  4308. public function testReplacelinksBelongsToManyWithJoint(): void
  4309. {
  4310. $table = $this->getTableLocator()->get('Articles');
  4311. $options = ['markNew' => false];
  4312. $article = new Entity(['id' => 1], $options);
  4313. $tags[] = new Tag([
  4314. 'id' => 2,
  4315. '_joinData' => new Entity([
  4316. 'article_id' => 1,
  4317. 'tag_id' => 2,
  4318. ]),
  4319. ], $options);
  4320. $tags[] = new Tag(['id' => 3], $options);
  4321. $table->getAssociation('Tags')->replaceLinks($article, $tags);
  4322. $this->assertSame($tags, $article->tags);
  4323. $article = $table->find('all')->where(['id' => 1])->contain(['Tags'])->first();
  4324. $this->assertCount(2, $article->tags);
  4325. $this->assertSame(2, $article->tags[0]->id);
  4326. $this->assertSame(3, $article->tags[1]->id);
  4327. }
  4328. /**
  4329. * Tests that options are being passed through to the internal table method calls.
  4330. */
  4331. public function testOptionsBeingPassedToImplicitBelongsToManyDeletesUsingSaveReplace(): void
  4332. {
  4333. $articles = $this->getTableLocator()->get('Articles');
  4334. $tags = $articles->Tags;
  4335. $tags->setSaveStrategy(BelongsToMany::SAVE_REPLACE)
  4336. ->setDependent(true)
  4337. ->setCascadeCallbacks(true);
  4338. $actualOptions = null;
  4339. $tags->junction()->getEventManager()->on(
  4340. 'Model.beforeDelete',
  4341. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4342. $actualOptions = $options->getArrayCopy();
  4343. }
  4344. );
  4345. $article = $articles->get(1);
  4346. $article->tags = [];
  4347. $article->setDirty('tags', true);
  4348. $result = $articles->save($article, ['foo' => 'bar']);
  4349. $this->assertNotEmpty($result);
  4350. $expected = [
  4351. '_primary' => false,
  4352. 'foo' => 'bar',
  4353. 'atomic' => true,
  4354. 'checkRules' => true,
  4355. 'checkExisting' => true,
  4356. '_cleanOnSuccess' => true,
  4357. ];
  4358. $this->assertEquals($expected, $actualOptions);
  4359. }
  4360. /**
  4361. * Tests that options are being passed through to the internal table method calls.
  4362. */
  4363. public function testOptionsBeingPassedToInternalSaveCallsUsingBelongsToManyLink(): void
  4364. {
  4365. $articles = $this->getTableLocator()->get('Articles');
  4366. $tags = $articles->Tags;
  4367. $actualOptions = null;
  4368. $tags->junction()->getEventManager()->on(
  4369. 'Model.beforeSave',
  4370. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4371. $actualOptions = $options->getArrayCopy();
  4372. }
  4373. );
  4374. $article = $articles->get(1);
  4375. $result = $tags->link($article, [$tags->getTarget()->get(2)], ['foo' => 'bar']);
  4376. $this->assertTrue($result);
  4377. $expected = [
  4378. '_primary' => true,
  4379. 'foo' => 'bar',
  4380. 'atomic' => true,
  4381. 'checkRules' => true,
  4382. 'checkExisting' => true,
  4383. 'associated' => [
  4384. 'Articles' => [],
  4385. 'Tags' => [],
  4386. ],
  4387. '_cleanOnSuccess' => true,
  4388. ];
  4389. $this->assertEquals($expected, $actualOptions);
  4390. }
  4391. /**
  4392. * Tests that options are being passed through to the internal table method calls.
  4393. */
  4394. public function testOptionsBeingPassedToInternalSaveCallsUsingBelongsToManyUnlink(): void
  4395. {
  4396. $articles = $this->getTableLocator()->get('Articles');
  4397. $tags = $articles->Tags;
  4398. $actualOptions = null;
  4399. $tags->junction()->getEventManager()->on(
  4400. 'Model.beforeDelete',
  4401. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4402. $actualOptions = $options->getArrayCopy();
  4403. }
  4404. );
  4405. $article = $articles->get(1);
  4406. $tags->unlink($article, [$tags->getTarget()->get(2)], ['foo' => 'bar']);
  4407. $expected = [
  4408. '_primary' => true,
  4409. 'foo' => 'bar',
  4410. 'atomic' => true,
  4411. 'checkRules' => true,
  4412. 'cleanProperty' => true,
  4413. ];
  4414. $this->assertEquals($expected, $actualOptions);
  4415. }
  4416. /**
  4417. * Tests that options are being passed through to the internal table method calls.
  4418. */
  4419. public function testOptionsBeingPassedToInternalSaveAndDeleteCallsUsingBelongsToManyReplaceLinks(): void
  4420. {
  4421. $articles = $this->getTableLocator()->get('Articles');
  4422. $tags = $articles->Tags;
  4423. $actualSaveOptions = null;
  4424. $actualDeleteOptions = null;
  4425. $tags->junction()->getEventManager()->on(
  4426. 'Model.beforeSave',
  4427. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualSaveOptions): void {
  4428. $actualSaveOptions = $options->getArrayCopy();
  4429. }
  4430. );
  4431. $tags->junction()->getEventManager()->on(
  4432. 'Model.beforeDelete',
  4433. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualDeleteOptions): void {
  4434. $actualDeleteOptions = $options->getArrayCopy();
  4435. }
  4436. );
  4437. $article = $articles->get(1);
  4438. $result = $tags->replaceLinks(
  4439. $article,
  4440. [
  4441. $tags->getTarget()->newEntity(['name' => 'new']),
  4442. $tags->getTarget()->get(2),
  4443. ],
  4444. ['foo' => 'bar']
  4445. );
  4446. $this->assertTrue($result);
  4447. $expected = [
  4448. '_primary' => true,
  4449. 'foo' => 'bar',
  4450. 'atomic' => true,
  4451. 'checkRules' => true,
  4452. 'checkExisting' => true,
  4453. 'associated' => [],
  4454. '_cleanOnSuccess' => true,
  4455. ];
  4456. $this->assertEquals($expected, $actualSaveOptions);
  4457. $expected = [
  4458. '_primary' => true,
  4459. 'foo' => 'bar',
  4460. 'atomic' => true,
  4461. 'checkRules' => true,
  4462. ];
  4463. $this->assertEquals($expected, $actualDeleteOptions);
  4464. }
  4465. /**
  4466. * Tests that options are being passed through to the internal table method calls.
  4467. */
  4468. public function testOptionsBeingPassedToImplicitHasManyDeletesUsingSaveReplace(): void
  4469. {
  4470. $authors = $this->getTableLocator()->get('Authors');
  4471. $articles = $authors->Articles;
  4472. $articles->setSaveStrategy(HasMany::SAVE_REPLACE)
  4473. ->setDependent(true)
  4474. ->setCascadeCallbacks(true);
  4475. $actualOptions = null;
  4476. $articles->getTarget()->getEventManager()->on(
  4477. 'Model.beforeDelete',
  4478. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4479. $actualOptions = $options->getArrayCopy();
  4480. }
  4481. );
  4482. $author = $authors->get(1);
  4483. $author->articles = [];
  4484. $author->setDirty('articles', true);
  4485. $result = $authors->save($author, ['foo' => 'bar']);
  4486. $this->assertNotEmpty($result);
  4487. $expected = [
  4488. '_primary' => false,
  4489. 'foo' => 'bar',
  4490. 'atomic' => true,
  4491. 'checkRules' => true,
  4492. 'checkExisting' => true,
  4493. '_sourceTable' => $authors,
  4494. '_cleanOnSuccess' => true,
  4495. ];
  4496. $this->assertEquals($expected, $actualOptions);
  4497. }
  4498. /**
  4499. * Tests that options are being passed through to the internal table method calls.
  4500. */
  4501. public function testOptionsBeingPassedToInternalSaveCallsUsingHasManyLink(): void
  4502. {
  4503. $authors = $this->getTableLocator()->get('Authors');
  4504. $articles = $authors->Articles;
  4505. $actualOptions = null;
  4506. $articles->getTarget()->getEventManager()->on(
  4507. 'Model.beforeSave',
  4508. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4509. $actualOptions = $options->getArrayCopy();
  4510. }
  4511. );
  4512. $author = $authors->get(1);
  4513. $author->articles = [];
  4514. $author->setDirty('articles', true);
  4515. $result = $articles->link($author, [$articles->getTarget()->get(2)], ['foo' => 'bar']);
  4516. $this->assertTrue($result);
  4517. $expected = [
  4518. '_primary' => true,
  4519. 'foo' => 'bar',
  4520. 'atomic' => true,
  4521. 'checkRules' => true,
  4522. 'checkExisting' => true,
  4523. '_sourceTable' => $authors,
  4524. 'associated' => [
  4525. 'Authors' => [],
  4526. 'Tags' => [],
  4527. 'ArticlesTags' => [],
  4528. ],
  4529. '_cleanOnSuccess' => true,
  4530. ];
  4531. $this->assertEquals($expected, $actualOptions);
  4532. }
  4533. /**
  4534. * Tests that options are being passed through to the internal table method calls.
  4535. */
  4536. public function testOptionsBeingPassedToInternalSaveCallsUsingHasManyUnlink(): void
  4537. {
  4538. $authors = $this->getTableLocator()->get('Authors');
  4539. $articles = $authors->Articles;
  4540. $articles->setDependent(true);
  4541. $articles->setCascadeCallbacks(true);
  4542. $actualOptions = null;
  4543. $articles->getTarget()->getEventManager()->on(
  4544. 'Model.beforeDelete',
  4545. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4546. $actualOptions = $options->getArrayCopy();
  4547. }
  4548. );
  4549. $author = $authors->get(1);
  4550. $author->articles = [];
  4551. $author->setDirty('articles', true);
  4552. $articles->unlink($author, [$articles->getTarget()->get(1)], ['foo' => 'bar']);
  4553. $expected = [
  4554. '_primary' => true,
  4555. 'foo' => 'bar',
  4556. 'atomic' => true,
  4557. 'checkRules' => true,
  4558. 'cleanProperty' => true,
  4559. ];
  4560. $this->assertEquals($expected, $actualOptions);
  4561. }
  4562. /**
  4563. * Tests that options are being passed through to the internal table method calls.
  4564. */
  4565. public function testOptionsBeingPassedToInternalSaveAndDeleteCallsUsingHasManyReplace(): void
  4566. {
  4567. $authors = $this->getTableLocator()->get('Authors');
  4568. $articles = $authors->Articles;
  4569. $articles->setDependent(true);
  4570. $articles->setCascadeCallbacks(true);
  4571. $actualSaveOptions = null;
  4572. $actualDeleteOptions = null;
  4573. $articles->getTarget()->getEventManager()->on(
  4574. 'Model.beforeSave',
  4575. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualSaveOptions): void {
  4576. $actualSaveOptions = $options->getArrayCopy();
  4577. }
  4578. );
  4579. $articles->getTarget()->getEventManager()->on(
  4580. 'Model.beforeDelete',
  4581. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualDeleteOptions): void {
  4582. $actualDeleteOptions = $options->getArrayCopy();
  4583. }
  4584. );
  4585. $author = $authors->get(1);
  4586. $result = $articles->replace(
  4587. $author,
  4588. [
  4589. $articles->getTarget()->newEntity(['title' => 'new', 'body' => 'new']),
  4590. $articles->getTarget()->get(1),
  4591. ],
  4592. ['foo' => 'bar']
  4593. );
  4594. $this->assertTrue($result);
  4595. $expected = [
  4596. '_primary' => true,
  4597. 'foo' => 'bar',
  4598. 'atomic' => true,
  4599. 'checkRules' => true,
  4600. 'checkExisting' => true,
  4601. '_sourceTable' => $authors,
  4602. 'associated' => [
  4603. 'Authors' => [],
  4604. 'Tags' => [],
  4605. 'ArticlesTags' => [],
  4606. ],
  4607. '_cleanOnSuccess' => true,
  4608. ];
  4609. $this->assertEquals($expected, $actualSaveOptions);
  4610. $expected = [
  4611. '_primary' => true,
  4612. 'foo' => 'bar',
  4613. 'atomic' => true,
  4614. 'checkRules' => true,
  4615. '_sourceTable' => $authors,
  4616. ];
  4617. $this->assertEquals($expected, $actualDeleteOptions);
  4618. }
  4619. /**
  4620. * Tests backwards compatibility of the the `$options` argument, formerly `$cleanProperty`.
  4621. */
  4622. public function testBackwardsCompatibilityForBelongsToManyUnlinkCleanPropertyOption(): void
  4623. {
  4624. $articles = $this->getTableLocator()->get('Articles');
  4625. $tags = $articles->Tags;
  4626. $actualOptions = null;
  4627. $tags->junction()->getEventManager()->on(
  4628. 'Model.beforeDelete',
  4629. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4630. $actualOptions = $options->getArrayCopy();
  4631. }
  4632. );
  4633. $article = $articles->get(1);
  4634. $tags->unlink($article, [$tags->getTarget()->get(1)], false);
  4635. $this->assertArrayHasKey('cleanProperty', $actualOptions);
  4636. $this->assertFalse($actualOptions['cleanProperty']);
  4637. $actualOptions = null;
  4638. $tags->unlink($article, [$tags->getTarget()->get(2)]);
  4639. $this->assertArrayHasKey('cleanProperty', $actualOptions);
  4640. $this->assertTrue($actualOptions['cleanProperty']);
  4641. }
  4642. /**
  4643. * Tests backwards compatibility of the the `$options` argument, formerly `$cleanProperty`.
  4644. */
  4645. public function testBackwardsCompatibilityForHasManyUnlinkCleanPropertyOption(): void
  4646. {
  4647. $authors = $this->getTableLocator()->get('Authors');
  4648. $articles = $authors->Articles;
  4649. $articles->setDependent(true);
  4650. $articles->setCascadeCallbacks(true);
  4651. $actualOptions = null;
  4652. $articles->getTarget()->getEventManager()->on(
  4653. 'Model.beforeDelete',
  4654. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$actualOptions): void {
  4655. $actualOptions = $options->getArrayCopy();
  4656. }
  4657. );
  4658. $author = $authors->get(1);
  4659. $author->articles = [];
  4660. $author->setDirty('articles', true);
  4661. $articles->unlink($author, [$articles->getTarget()->get(1)], false);
  4662. $this->assertArrayHasKey('cleanProperty', $actualOptions);
  4663. $this->assertFalse($actualOptions['cleanProperty']);
  4664. $actualOptions = null;
  4665. $articles->unlink($author, [$articles->getTarget()->get(3)]);
  4666. $this->assertArrayHasKey('cleanProperty', $actualOptions);
  4667. $this->assertTrue($actualOptions['cleanProperty']);
  4668. }
  4669. /**
  4670. * Tests that it is possible to call find with no arguments
  4671. */
  4672. public function testSimplifiedFind(): void
  4673. {
  4674. $table = $this->getMockBuilder(Table::class)
  4675. ->onlyMethods(['findAll'])
  4676. ->setConstructorArgs([[
  4677. 'connection' => $this->connection,
  4678. 'schema' => ['id' => ['type' => 'integer']],
  4679. ]])
  4680. ->getMock();
  4681. $table->expects($this->once())->method('findAll');
  4682. $table->find();
  4683. }
  4684. public static function providerForTestGet(): array
  4685. {
  4686. return [
  4687. [['fields' => ['id']]],
  4688. [['fields' => ['id'], 'cache' => null]],
  4689. ];
  4690. }
  4691. /**
  4692. * Test that get() will use the primary key for searching and return the first
  4693. * entity found
  4694. *
  4695. * @dataProvider providerForTestGet
  4696. * @param array $options
  4697. */
  4698. public function testGet($options): void
  4699. {
  4700. $table = $this->getMockBuilder(Table::class)
  4701. ->onlyMethods(['selectQuery'])
  4702. ->setConstructorArgs([[
  4703. 'connection' => $this->connection,
  4704. 'schema' => [
  4705. 'id' => ['type' => 'integer'],
  4706. 'bar' => ['type' => 'integer'],
  4707. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['bar']]],
  4708. ],
  4709. ]])
  4710. ->getMock();
  4711. $query = $this->getMockBuilder(SelectQuery::class)
  4712. ->onlyMethods(['addDefaultTypes', 'firstOrFail', 'where', 'cache', 'applyOptions'])
  4713. ->setConstructorArgs([$table])
  4714. ->getMock();
  4715. $table->expects($this->once())->method('selectQuery')
  4716. ->willReturn($query);
  4717. $entity = new Entity();
  4718. $query->expects($this->once())->method('applyOptions')
  4719. ->with(['fields' => ['id']]);
  4720. $query->expects($this->once())->method('where')
  4721. ->with([$table->getAlias() . '.bar' => 10])
  4722. ->willReturnSelf();
  4723. $query->expects($this->never())->method('cache');
  4724. $query->expects($this->once())->method('firstOrFail')
  4725. ->willReturn($entity);
  4726. $result = $table->get(10, ...$options);
  4727. $this->assertSame($entity, $result);
  4728. }
  4729. public static function providerForTestGetWithCache(): array
  4730. {
  4731. return [
  4732. [
  4733. ['fields' => ['id'], 'cache' => 'default'],
  4734. 'get-test-table_name-[10]', 'default', 10,
  4735. ],
  4736. [
  4737. ['fields' => ['id'], 'cache' => 'default'],
  4738. 'get-test-table_name-["uuid"]', 'default', 'uuid',
  4739. ],
  4740. [
  4741. ['fields' => ['id'], 'cache' => 'default'],
  4742. 'get-test-table_name-["2020-07-08T00:00:00+00:00"]', 'default', new DateTime('2020-07-08'),
  4743. ],
  4744. [
  4745. ['fields' => ['id'], 'cache' => 'default', 'cacheKey' => 'custom_key'],
  4746. 'custom_key', 'default', 10,
  4747. ],
  4748. ];
  4749. }
  4750. /**
  4751. * Test that get() will use the cache.
  4752. *
  4753. * @dataProvider providerForTestGetWithCache
  4754. * @param array $options
  4755. * @param string $cacheKey
  4756. * @param string $cacheConfig
  4757. * @param mixed $primaryKey
  4758. */
  4759. public function testGetWithCache($options, $cacheKey, $cacheConfig, $primaryKey): void
  4760. {
  4761. $table = $this->getMockBuilder(Table::class)
  4762. ->onlyMethods(['selectQuery'])
  4763. ->setConstructorArgs([[
  4764. 'connection' => $this->connection,
  4765. 'schema' => [
  4766. 'id' => ['type' => 'integer'],
  4767. 'bar' => ['type' => 'integer'],
  4768. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['bar']]],
  4769. ],
  4770. ]])
  4771. ->getMock();
  4772. $table->setTable('table_name');
  4773. $query = $this->getMockBuilder(SelectQuery::class)
  4774. ->onlyMethods(['addDefaultTypes', 'firstOrFail', 'where', 'cache', 'applyOptions'])
  4775. ->setConstructorArgs([$table])
  4776. ->getMock();
  4777. $table->expects($this->once())->method('selectQuery')
  4778. ->willReturn($query);
  4779. $entity = new Entity();
  4780. $query->expects($this->once())->method('applyOptions')
  4781. ->with(['fields' => ['id']]);
  4782. $query->expects($this->once())->method('where')
  4783. ->with([$table->getAlias() . '.bar' => $primaryKey])
  4784. ->willReturnSelf();
  4785. $query->expects($this->once())->method('cache')
  4786. ->with($cacheKey, $cacheConfig)
  4787. ->willReturnSelf();
  4788. $query->expects($this->once())->method('firstOrFail')
  4789. ->willReturn($entity);
  4790. $result = $table->get($primaryKey, ...$options);
  4791. $this->assertSame($entity, $result);
  4792. }
  4793. /**
  4794. * Test get() with options array.
  4795. *
  4796. * @return void
  4797. */
  4798. #[WithoutErrorHandler]
  4799. public function testGetBackwardsCompatibility(): void
  4800. {
  4801. $this->deprecated(function () {
  4802. $table = $this->getTableLocator()->get('Articles');
  4803. $article = $table->get(1, ['contain' => 'Authors']);
  4804. $this->assertNotEmpty($article->author);
  4805. });
  4806. }
  4807. /**
  4808. * Tests that get() will throw an exception if the record was not found
  4809. */
  4810. public function testGetNotFoundException(): void
  4811. {
  4812. $this->expectException(RecordNotFoundException::class);
  4813. $this->expectExceptionMessage('Record not found in table `articles`.');
  4814. $table = new Table([
  4815. 'name' => 'Articles',
  4816. 'connection' => $this->connection,
  4817. 'table' => 'articles',
  4818. ]);
  4819. $table->get(10);
  4820. }
  4821. /**
  4822. * Test that an exception is raised when there are not enough keys.
  4823. */
  4824. public function testGetExceptionOnNoData(): void
  4825. {
  4826. $this->expectException(InvalidPrimaryKeyException::class);
  4827. $this->expectExceptionMessage('Record not found in table `articles` with primary key `[NULL]`.');
  4828. $table = new Table([
  4829. 'name' => 'Articles',
  4830. 'connection' => $this->connection,
  4831. 'table' => 'articles',
  4832. ]);
  4833. $table->get(null);
  4834. }
  4835. /**
  4836. * Test that an exception is raised when there are too many keys.
  4837. */
  4838. public function testGetExceptionOnTooMuchData(): void
  4839. {
  4840. $this->expectException(InvalidPrimaryKeyException::class);
  4841. $this->expectExceptionMessage('Record not found in table `articles` with primary key `[1, \'two\']`.');
  4842. $table = new Table([
  4843. 'name' => 'Articles',
  4844. 'connection' => $this->connection,
  4845. 'table' => 'articles',
  4846. ]);
  4847. $table->get([1, 'two']);
  4848. }
  4849. /**
  4850. * Tests that patchEntity delegates the task to the marshaller and passed
  4851. * all associations
  4852. */
  4853. public function testPatchEntityMarshallerUsage(): void
  4854. {
  4855. $table = $this->getMockBuilder(Table::class)
  4856. ->onlyMethods(['marshaller'])
  4857. ->getMock();
  4858. $marshaller = $this->getMockBuilder('Cake\ORM\Marshaller')
  4859. ->setConstructorArgs([$table])
  4860. ->getMock();
  4861. $table->belongsTo('users');
  4862. $table->hasMany('articles');
  4863. $table->expects($this->once())->method('marshaller')
  4864. ->willReturn($marshaller);
  4865. $entity = new Entity();
  4866. $data = ['foo' => 'bar'];
  4867. $marshaller->expects($this->once())
  4868. ->method('merge')
  4869. ->with($entity, $data, ['associated' => ['users', 'articles']])
  4870. ->willReturn($entity);
  4871. $table->patchEntity($entity, $data);
  4872. }
  4873. /**
  4874. * Tests patchEntity in a simple scenario. The tests for Marshaller cover
  4875. * patch scenarios in more depth.
  4876. */
  4877. public function testPatchEntity(): void
  4878. {
  4879. $table = $this->getTableLocator()->get('Articles');
  4880. $entity = new Entity(['title' => 'old title'], ['markNew' => false]);
  4881. $data = ['title' => 'new title'];
  4882. $entity = $table->patchEntity($entity, $data);
  4883. $this->assertSame($data['title'], $entity->title);
  4884. $this->assertFalse($entity->isNew(), 'entity should not be new.');
  4885. }
  4886. /**
  4887. * Tests that patchEntities delegates the task to the marshaller and passed
  4888. * all associations
  4889. */
  4890. public function testPatchEntitiesMarshallerUsage(): void
  4891. {
  4892. $table = $this->getMockBuilder(Table::class)
  4893. ->onlyMethods(['marshaller'])
  4894. ->getMock();
  4895. $marshaller = $this->getMockBuilder('Cake\ORM\Marshaller')
  4896. ->setConstructorArgs([$table])
  4897. ->getMock();
  4898. $table->belongsTo('users');
  4899. $table->hasMany('articles');
  4900. $table->expects($this->once())->method('marshaller')
  4901. ->willReturn($marshaller);
  4902. $entities = [new Entity()];
  4903. $data = [['foo' => 'bar']];
  4904. $marshaller->expects($this->once())
  4905. ->method('mergeMany')
  4906. ->with($entities, $data, ['associated' => ['users', 'articles']])
  4907. ->willReturn($entities);
  4908. $table->patchEntities($entities, $data);
  4909. }
  4910. /**
  4911. * Tests patchEntities in a simple scenario. The tests for Marshaller cover
  4912. * patch scenarios in more depth.
  4913. */
  4914. public function testPatchEntities(): void
  4915. {
  4916. $table = $this->getTableLocator()->get('Articles');
  4917. $entities = $table->find()->limit(2)->toArray();
  4918. $data = [
  4919. ['id' => $entities[0]->id, 'title' => 'new title'],
  4920. ['id' => $entities[1]->id, 'title' => 'new title2'],
  4921. ];
  4922. $entities = $table->patchEntities($entities, $data);
  4923. foreach ($entities as $i => $entity) {
  4924. $this->assertFalse($entity->isNew(), 'entities should not be new.');
  4925. $this->assertSame($data[$i]['title'], $entity->title);
  4926. }
  4927. }
  4928. /**
  4929. * Tests __debugInfo
  4930. */
  4931. public function testDebugInfo(): void
  4932. {
  4933. $articles = $this->getTableLocator()->get('articles');
  4934. $articles->addBehavior('Timestamp');
  4935. $result = $articles->__debugInfo();
  4936. $expected = [
  4937. 'registryAlias' => 'articles',
  4938. 'table' => 'articles',
  4939. 'alias' => 'articles',
  4940. 'entityClass' => 'TestApp\Model\Entity\Article',
  4941. 'associations' => ['Authors', 'Tags', 'ArticlesTags'],
  4942. 'behaviors' => ['Timestamp'],
  4943. 'defaultConnection' => 'default',
  4944. 'connectionName' => 'test',
  4945. ];
  4946. $this->assertEquals($expected, $result);
  4947. $articles = $this->getTableLocator()->get('Foo.Articles');
  4948. $result = $articles->__debugInfo();
  4949. $expected = [
  4950. 'registryAlias' => 'Foo.Articles',
  4951. 'table' => 'articles',
  4952. 'alias' => 'Articles',
  4953. 'entityClass' => 'Cake\ORM\Entity',
  4954. 'associations' => [],
  4955. 'behaviors' => [],
  4956. 'defaultConnection' => 'default',
  4957. 'connectionName' => 'test',
  4958. ];
  4959. $this->assertEquals($expected, $result);
  4960. }
  4961. /**
  4962. * Test that findOrCreate creates a new entity, and then finds that entity.
  4963. */
  4964. public function testFindOrCreateNewEntity(): void
  4965. {
  4966. $articles = $this->getTableLocator()->get('Articles');
  4967. $callbackExecuted = false;
  4968. $firstArticle = $articles->findOrCreate(['title' => 'Not there'], function ($article) use (&$callbackExecuted): void {
  4969. $this->assertInstanceOf(EntityInterface::class, $article);
  4970. $article->body = 'New body';
  4971. $callbackExecuted = true;
  4972. });
  4973. $this->assertTrue($callbackExecuted);
  4974. $this->assertFalse($firstArticle->isNew());
  4975. $this->assertNotNull($firstArticle->id);
  4976. $this->assertSame('Not there', $firstArticle->title);
  4977. $this->assertSame('New body', $firstArticle->body);
  4978. $secondArticle = $articles->findOrCreate(['title' => 'Not there'], function ($article): void {
  4979. $this->fail('Should not be called for existing entities.');
  4980. });
  4981. $this->assertFalse($secondArticle->isNew());
  4982. $this->assertNotNull($secondArticle->id);
  4983. $this->assertSame('Not there', $secondArticle->title);
  4984. $this->assertEquals($firstArticle->id, $secondArticle->id);
  4985. }
  4986. /**
  4987. * Test that findOrCreate finds fixture data.
  4988. */
  4989. public function testFindOrCreateExistingEntity(): void
  4990. {
  4991. $articles = $this->getTableLocator()->get('Articles');
  4992. $article = $articles->findOrCreate(['title' => 'First Article'], function ($article): void {
  4993. $this->fail('Should not be called for existing entities.');
  4994. });
  4995. $this->assertFalse($article->isNew());
  4996. $this->assertNotNull($article->id);
  4997. $this->assertSame('First Article', $article->title);
  4998. }
  4999. /**
  5000. * Test that findOrCreate uses the search conditions as defaults for new entity.
  5001. */
  5002. public function testFindOrCreateDefaults(): void
  5003. {
  5004. $articles = $this->getTableLocator()->get('Articles');
  5005. $callbackExecuted = false;
  5006. $article = $articles->findOrCreate(
  5007. ['author_id' => 2, 'title' => 'First Article'],
  5008. function ($article) use (&$callbackExecuted): void {
  5009. $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
  5010. $article->set(['published' => 'N', 'body' => 'New body']);
  5011. $callbackExecuted = true;
  5012. }
  5013. );
  5014. $this->assertTrue($callbackExecuted);
  5015. $this->assertFalse($article->isNew());
  5016. $this->assertNotNull($article->id);
  5017. $this->assertSame('First Article', $article->title);
  5018. $this->assertSame('New body', $article->body);
  5019. $this->assertSame('N', $article->published);
  5020. $this->assertSame(2, $article->author_id);
  5021. $query = $articles->find()->where(['author_id' => 2, 'title' => 'First Article']);
  5022. $article = $articles->findOrCreate($query);
  5023. $this->assertSame('First Article', $article->title);
  5024. $this->assertSame(2, $article->author_id);
  5025. $this->assertFalse($article->isNew());
  5026. }
  5027. /**
  5028. * Test that findOrCreate adds new entity without using a callback.
  5029. */
  5030. public function testFindOrCreateNoCallable(): void
  5031. {
  5032. $articles = $this->getTableLocator()->get('Articles');
  5033. $article = $articles->findOrCreate(['title' => 'Just Something New']);
  5034. $this->assertFalse($article->isNew());
  5035. $this->assertNotNull($article->id);
  5036. $this->assertSame('Just Something New', $article->title);
  5037. }
  5038. /**
  5039. * Test that findOrCreate executes search conditions as a callable.
  5040. */
  5041. public function testFindOrCreateSearchCallable(): void
  5042. {
  5043. $articles = $this->getTableLocator()->get('Articles');
  5044. $calledOne = false;
  5045. $calledTwo = false;
  5046. $article = $articles->findOrCreate(function ($query) use (&$calledOne): void {
  5047. $this->assertInstanceOf('Cake\ORM\Query', $query);
  5048. $query->where(['title' => 'Something Else']);
  5049. $calledOne = true;
  5050. }, function ($article) use (&$calledTwo): void {
  5051. $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
  5052. $article->title = 'Set Defaults Here';
  5053. $calledTwo = true;
  5054. });
  5055. $this->assertTrue($calledOne);
  5056. $this->assertTrue($calledTwo);
  5057. $this->assertFalse($article->isNew());
  5058. $this->assertNotNull($article->id);
  5059. $this->assertSame('Set Defaults Here', $article->title);
  5060. }
  5061. /**
  5062. * Test that findOrCreate options disable defaults.
  5063. */
  5064. public function testFindOrCreateNoDefaults(): void
  5065. {
  5066. $articles = $this->getTableLocator()->get('Articles');
  5067. $article = $articles->findOrCreate(['title' => 'A New Article', 'published' => 'Y'], function ($article): void {
  5068. $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
  5069. $article->title = 'A Different Title';
  5070. }, ['defaults' => false]);
  5071. $this->assertFalse($article->isNew());
  5072. $this->assertNotNull($article->id);
  5073. $this->assertSame('A Different Title', $article->title);
  5074. $this->assertNull($article->published, 'Expected Null since defaults are disabled.');
  5075. }
  5076. /**
  5077. * Test that findOrCreate executes callable inside transaction.
  5078. */
  5079. public function testFindOrCreateTransactions(): void
  5080. {
  5081. $articles = $this->getTableLocator()->get('Articles');
  5082. $articles->getEventManager()->on('Model.afterSaveCommit', function (EventInterface $event, EntityInterface $entity, ArrayObject $options): void {
  5083. $entity->afterSaveCommit = true;
  5084. });
  5085. $article = $articles->findOrCreate(function ($query): void {
  5086. $this->assertInstanceOf('Cake\ORM\Query', $query);
  5087. $query->where(['title' => 'Find Something New']);
  5088. $this->assertTrue($this->connection->inTransaction());
  5089. }, function ($article): void {
  5090. $this->assertInstanceOf('Cake\Datasource\EntityInterface', $article);
  5091. $article->title = 'Success';
  5092. $this->assertTrue($this->connection->inTransaction());
  5093. });
  5094. $this->assertFalse($article->isNew());
  5095. $this->assertNotNull($article->id);
  5096. $this->assertSame('Success', $article->title);
  5097. $this->assertTrue($article->afterSaveCommit);
  5098. }
  5099. /**
  5100. * Test that findOrCreate executes callable without transaction.
  5101. */
  5102. public function testFindOrCreateNoTransaction(): void
  5103. {
  5104. $articles = $this->getTableLocator()->get('Articles');
  5105. $article = $articles->findOrCreate(function (SelectQuery $query): void {
  5106. $this->assertInstanceOf(SelectQuery::class, $query);
  5107. $query->where(['title' => 'Find Something New']);
  5108. $this->assertFalse($this->connection->inTransaction());
  5109. }, function ($article): void {
  5110. $this->assertInstanceOf(EntityInterface::class, $article);
  5111. $this->assertFalse($this->connection->inTransaction());
  5112. $article->title = 'Success';
  5113. }, ['atomic' => false]);
  5114. $this->assertFalse($article->isNew());
  5115. $this->assertNotNull($article->id);
  5116. $this->assertSame('Success', $article->title);
  5117. }
  5118. /**
  5119. * Test that findOrCreate throws a PersistenceFailedException when it cannot save
  5120. * an entity created from $search
  5121. */
  5122. public function testFindOrCreateWithInvalidEntity(): void
  5123. {
  5124. $this->expectException(PersistenceFailedException::class);
  5125. $this->expectExceptionMessage(
  5126. 'Entity findOrCreate failure. ' .
  5127. 'Found the following errors (title._empty: "This field cannot be left empty").'
  5128. );
  5129. $articles = $this->getTableLocator()->get('Articles');
  5130. $validator = new Validator();
  5131. $validator->notEmptyString('title');
  5132. $articles->setValidator('default', $validator);
  5133. $articles->findOrCreate(['title' => '']);
  5134. }
  5135. /**
  5136. * Test that findOrCreate allows patching of all $search keys
  5137. */
  5138. public function testFindOrCreateAccessibleFields(): void
  5139. {
  5140. $articles = $this->getTableLocator()->get('Articles');
  5141. $articles->setEntityClass(ProtectedEntity::class);
  5142. $validator = new Validator();
  5143. $validator->notBlank('title');
  5144. $articles->setValidator('default', $validator);
  5145. $article = $articles->findOrCreate(['title' => 'test']);
  5146. $this->assertInstanceOf(ProtectedEntity::class, $article);
  5147. $this->assertSame('test', $article->title);
  5148. }
  5149. /**
  5150. * Test that findOrCreate cannot accidentally bypass required validation.
  5151. */
  5152. public function testFindOrCreatePartialValidation(): void
  5153. {
  5154. $articles = $this->getTableLocator()->get('Articles');
  5155. $articles->setEntityClass(ProtectedEntity::class);
  5156. $validator = new Validator();
  5157. $validator->notBlank('title')->requirePresence('title', 'create');
  5158. $validator->notBlank('body')->requirePresence('body', 'create');
  5159. $articles->setValidator('default', $validator);
  5160. $this->expectException(PersistenceFailedException::class);
  5161. $this->expectExceptionMessage(
  5162. 'Entity findOrCreate failure. ' .
  5163. 'Found the following errors (title._required: "This field is required").'
  5164. );
  5165. $articles->findOrCreate(['body' => 'test']);
  5166. }
  5167. /**
  5168. * Test that creating a table fires the initialize event.
  5169. */
  5170. public function testInitializeEvent(): void
  5171. {
  5172. $count = 0;
  5173. $cb = function (EventInterface $event) use (&$count): void {
  5174. $count++;
  5175. };
  5176. EventManager::instance()->on('Model.initialize', $cb);
  5177. $this->getTableLocator()->get('Articles');
  5178. $this->assertSame(1, $count, 'Callback should be called');
  5179. EventManager::instance()->off('Model.initialize', $cb);
  5180. }
  5181. /**
  5182. * Tests the hasFinder method
  5183. */
  5184. public function testHasFinder(): void
  5185. {
  5186. $table = $this->getTableLocator()->get('articles');
  5187. $table->addBehavior('Sluggable');
  5188. $this->assertTrue($table->hasFinder('list'));
  5189. $this->assertTrue($table->hasFinder('noSlug'));
  5190. $this->assertFalse($table->hasFinder('noFind'));
  5191. }
  5192. /**
  5193. * Tests that calling validator() trigger the buildValidator event
  5194. */
  5195. public function testBuildValidatorEvent(): void
  5196. {
  5197. $count = 0;
  5198. $cb = function (EventInterface $event) use (&$count): void {
  5199. $count++;
  5200. };
  5201. EventManager::instance()->on('Model.buildValidator', $cb);
  5202. $articles = $this->getTableLocator()->get('Articles');
  5203. $articles->getValidator();
  5204. $this->assertSame(1, $count, 'Callback should be called');
  5205. $articles->getValidator();
  5206. $this->assertSame(1, $count, 'Callback should be called only once');
  5207. }
  5208. /**
  5209. * Tests the validateUnique method with different combinations
  5210. */
  5211. public function testValidateUnique(): void
  5212. {
  5213. $table = $this->getTableLocator()->get('Users');
  5214. $validator = new Validator();
  5215. $validator->add('username', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);
  5216. $validator->setProvider('table', $table);
  5217. $data = ['username' => ['larry', 'notthere']];
  5218. $this->assertNotEmpty($validator->validate($data));
  5219. $data = ['username' => 'larry'];
  5220. $this->assertNotEmpty($validator->validate($data));
  5221. $data = ['username' => 'jose'];
  5222. $this->assertEmpty($validator->validate($data));
  5223. $data = ['username' => 'larry', 'id' => 3];
  5224. $this->assertEmpty($validator->validate($data, false));
  5225. $data = ['username' => 'larry', 'id' => 3];
  5226. $this->assertNotEmpty($validator->validate($data));
  5227. $data = ['username' => 'larry'];
  5228. $this->assertNotEmpty($validator->validate($data, false));
  5229. $validator->add('username', 'unique', [
  5230. 'rule' => 'validateUnique', 'provider' => 'table',
  5231. ]);
  5232. $data = ['username' => 'larry'];
  5233. $this->assertNotEmpty($validator->validate($data, false));
  5234. }
  5235. /**
  5236. * Tests the validateUnique method with scope
  5237. */
  5238. public function testValidateUniqueScope(): void
  5239. {
  5240. $table = $this->getTableLocator()->get('Users');
  5241. $validator = new Validator();
  5242. $validator->add('username', 'unique', [
  5243. 'rule' => ['validateUnique', ['derp' => 'erp', 'scope' => 'id']],
  5244. 'provider' => 'table',
  5245. ]);
  5246. $validator->setProvider('table', $table);
  5247. $data = ['username' => 'larry', 'id' => 3];
  5248. $this->assertNotEmpty($validator->validate($data));
  5249. $data = ['username' => 'larry', 'id' => 1];
  5250. $this->assertEmpty($validator->validate($data));
  5251. $data = ['username' => 'jose'];
  5252. $this->assertEmpty($validator->validate($data));
  5253. }
  5254. /**
  5255. * Tests the validateUnique method with options
  5256. */
  5257. public function testValidateUniqueMultipleNulls(): void
  5258. {
  5259. $entity = new Entity([
  5260. 'id' => 9,
  5261. 'site_id' => 1,
  5262. 'author_id' => null,
  5263. 'title' => 'Null title',
  5264. ]);
  5265. $table = $this->getTableLocator()->get('SiteArticles');
  5266. $table->save($entity);
  5267. $validator = new Validator();
  5268. $validator->add('site_id', 'unique', [
  5269. 'rule' => [
  5270. 'validateUnique',
  5271. [
  5272. 'allowMultipleNulls' => false,
  5273. 'scope' => ['author_id'],
  5274. ],
  5275. ],
  5276. 'provider' => 'table',
  5277. 'message' => 'Must be unique.',
  5278. ]);
  5279. $validator->setProvider('table', $table);
  5280. $data = ['site_id' => 1, 'author_id' => null, 'title' => 'Null dupe'];
  5281. $expected = ['site_id' => ['unique' => 'Must be unique.']];
  5282. $this->assertEquals($expected, $validator->validate($data));
  5283. }
  5284. /**
  5285. * Tests that the callbacks receive the expected types of arguments.
  5286. */
  5287. public function testCallbackArgumentTypes(): void
  5288. {
  5289. $table = $this->getTableLocator()->get('articles');
  5290. $table->belongsTo('authors');
  5291. $eventManager = $table->getEventManager();
  5292. $associationBeforeFindCount = 0;
  5293. $table->getAssociation('authors')->getTarget()->getEventManager()->on(
  5294. 'Model.beforeFind',
  5295. function (EventInterface $event, SelectQuery $query, ArrayObject $options, bool $primary) use (&$associationBeforeFindCount): void {
  5296. $this->assertIsBool($primary);
  5297. $associationBeforeFindCount++;
  5298. }
  5299. );
  5300. $beforeFindCount = 0;
  5301. $eventManager->on(
  5302. 'Model.beforeFind',
  5303. function (EventInterface $event, SelectQuery $query, ArrayObject $options, bool $primary) use (&$beforeFindCount): void {
  5304. $this->assertIsBool($primary);
  5305. $beforeFindCount++;
  5306. }
  5307. );
  5308. $table->find()->contain('authors')->first();
  5309. $this->assertSame(1, $associationBeforeFindCount);
  5310. $this->assertSame(1, $beforeFindCount);
  5311. $buildValidatorCount = 0;
  5312. $eventManager->on(
  5313. 'Model.buildValidator',
  5314. $callback = function (EventInterface $event, Validator $validator, $name) use (&$buildValidatorCount): void {
  5315. $this->assertIsString($name);
  5316. $buildValidatorCount++;
  5317. }
  5318. );
  5319. $table->getValidator();
  5320. $this->assertSame(1, $buildValidatorCount);
  5321. $buildRulesCount =
  5322. $beforeRulesCount =
  5323. $afterRulesCount =
  5324. $beforeSaveCount =
  5325. $afterSaveCount = 0;
  5326. $eventManager->on(
  5327. 'Model.buildRules',
  5328. function (EventInterface $event, RulesChecker $rules) use (&$buildRulesCount): void {
  5329. $buildRulesCount++;
  5330. }
  5331. );
  5332. $eventManager->on(
  5333. 'Model.beforeRules',
  5334. function (EventInterface $event, EntityInterface $entity, ArrayObject $options, $operation) use (&$beforeRulesCount): void {
  5335. $this->assertIsString($operation);
  5336. $beforeRulesCount++;
  5337. }
  5338. );
  5339. $eventManager->on(
  5340. 'Model.afterRules',
  5341. function (EventInterface $event, EntityInterface $entity, ArrayObject $options, $result, $operation) use (&$afterRulesCount): void {
  5342. $this->assertIsBool($result);
  5343. $this->assertIsString($operation);
  5344. $afterRulesCount++;
  5345. }
  5346. );
  5347. $eventManager->on(
  5348. 'Model.beforeSave',
  5349. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$beforeSaveCount): void {
  5350. $beforeSaveCount++;
  5351. }
  5352. );
  5353. $eventManager->on(
  5354. 'Model.afterSave',
  5355. $afterSaveCallback = function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$afterSaveCount): void {
  5356. $afterSaveCount++;
  5357. }
  5358. );
  5359. $entity = new Entity(['title' => 'Title']);
  5360. $this->assertNotFalse($table->save($entity));
  5361. $this->assertSame(1, $buildRulesCount);
  5362. $this->assertSame(1, $beforeRulesCount);
  5363. $this->assertSame(1, $afterRulesCount);
  5364. $this->assertSame(1, $beforeSaveCount);
  5365. $this->assertSame(1, $afterSaveCount);
  5366. $beforeDeleteCount =
  5367. $afterDeleteCount = 0;
  5368. $eventManager->on(
  5369. 'Model.beforeDelete',
  5370. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$beforeDeleteCount): void {
  5371. $beforeDeleteCount++;
  5372. }
  5373. );
  5374. $eventManager->on(
  5375. 'Model.afterDelete',
  5376. function (EventInterface $event, EntityInterface $entity, ArrayObject $options) use (&$afterDeleteCount): void {
  5377. $afterDeleteCount++;
  5378. }
  5379. );
  5380. $this->assertTrue($table->delete($entity, ['checkRules' => false]));
  5381. $this->assertSame(1, $beforeDeleteCount);
  5382. $this->assertSame(1, $afterDeleteCount);
  5383. }
  5384. /**
  5385. * Tests that calling newEmptyEntity() on a table sets the right source alias.
  5386. */
  5387. public function testSetEntitySource(): void
  5388. {
  5389. $table = $this->getTableLocator()->get('Articles');
  5390. $this->assertSame('Articles', $table->newEmptyEntity()->getSource());
  5391. $this->loadPlugins(['TestPlugin']);
  5392. $table = $this->getTableLocator()->get('TestPlugin.Comments');
  5393. $this->assertSame('TestPlugin.Comments', $table->newEmptyEntity()->getSource());
  5394. }
  5395. /**
  5396. * Tests that passing a coned entity that was marked as new to save() will
  5397. * actually save it as a new entity
  5398. *
  5399. * @group save
  5400. */
  5401. public function testSaveWithClonedEntity(): void
  5402. {
  5403. $table = $this->getTableLocator()->get('Articles');
  5404. $article = $table->get(1);
  5405. $cloned = clone $article;
  5406. $cloned->unset('id');
  5407. $cloned->setNew(true);
  5408. $this->assertSame($cloned, $table->save($cloned));
  5409. $this->assertEquals(
  5410. $article->extract(['title', 'author_id']),
  5411. $cloned->extract(['title', 'author_id'])
  5412. );
  5413. $this->assertSame(4, $cloned->id);
  5414. }
  5415. /**
  5416. * Tests that the _ids notation can be used for HasMany
  5417. */
  5418. public function testSaveHasManyWithIds(): void
  5419. {
  5420. $data = [
  5421. 'username' => 'lux',
  5422. 'password' => 'passphrase',
  5423. 'comments' => [
  5424. '_ids' => [1, 2],
  5425. ],
  5426. ];
  5427. $userTable = $this->getTableLocator()->get('Users');
  5428. $userTable->hasMany('Comments');
  5429. $savedUser = $userTable->save($userTable->newEntity($data, ['associated' => ['Comments']]));
  5430. $retrievedUser = $userTable->find('all')->where(['id' => $savedUser->id])->contain(['Comments'])->first();
  5431. $this->assertEquals($savedUser->comments[0]->user_id, $retrievedUser->comments[0]->user_id);
  5432. $this->assertEquals($savedUser->comments[1]->user_id, $retrievedUser->comments[1]->user_id);
  5433. }
  5434. /**
  5435. * Tests that on second save, entities for the has many relation are not marked
  5436. * as dirty unnecessarily. This helps avoid wasteful database statements and makes
  5437. * for a cleaner transaction log
  5438. */
  5439. public function testSaveHasManyNoWasteSave(): void
  5440. {
  5441. $data = [
  5442. 'username' => 'lux',
  5443. 'password' => 'passphrase',
  5444. 'comments' => [
  5445. '_ids' => [1, 2],
  5446. ],
  5447. ];
  5448. $userTable = $this->getTableLocator()->get('Users');
  5449. $userTable->hasMany('Comments');
  5450. $savedUser = $userTable->save($userTable->newEntity($data, ['associated' => ['Comments']]));
  5451. $counter = 0;
  5452. $userTable->Comments
  5453. ->getEventManager()
  5454. ->on('Model.afterSave', function (EventInterface $event, $entity) use (&$counter): void {
  5455. if ($entity->isDirty()) {
  5456. $counter++;
  5457. }
  5458. });
  5459. $savedUser->comments[] = $userTable->Comments->get(5);
  5460. $this->assertCount(3, $savedUser->comments);
  5461. $savedUser->setDirty('comments', true);
  5462. $userTable->save($savedUser);
  5463. $this->assertSame(1, $counter);
  5464. }
  5465. /**
  5466. * Tests that on second save, entities for the belongsToMany relation are not marked
  5467. * as dirty unnecessarily. This helps avoid wasteful database statements and makes
  5468. * for a cleaner transaction log
  5469. */
  5470. public function testSaveBelongsToManyNoWasteSave(): void
  5471. {
  5472. $data = [
  5473. 'title' => 'foo',
  5474. 'body' => 'bar',
  5475. 'tags' => [
  5476. '_ids' => [1, 2],
  5477. ],
  5478. ];
  5479. $table = $this->getTableLocator()->get('Articles');
  5480. $article = $table->save($table->newEntity($data, ['associated' => ['Tags']]));
  5481. $counter = 0;
  5482. $table->Tags->junction()
  5483. ->getEventManager()
  5484. ->on('Model.afterSave', function (EventInterface $event, $entity) use (&$counter): void {
  5485. if ($entity->isDirty()) {
  5486. $counter++;
  5487. }
  5488. });
  5489. $article->tags[] = $table->Tags->get(3);
  5490. $this->assertCount(3, $article->tags);
  5491. $article->setDirty('tags', true);
  5492. $table->save($article);
  5493. $this->assertSame(1, $counter);
  5494. }
  5495. /**
  5496. * Tests that after saving then entity contains the right primary
  5497. * key casted to the right type
  5498. *
  5499. * @group save
  5500. */
  5501. public function testSaveCorrectPrimaryKeyType(): void
  5502. {
  5503. $entity = new Entity([
  5504. 'username' => 'superuser',
  5505. 'created' => new DateTime('2013-10-10 00:00'),
  5506. 'updated' => new DateTime('2013-10-10 00:00'),
  5507. ], ['markNew' => true]);
  5508. $table = $this->getTableLocator()->get('Users');
  5509. $this->assertSame($entity, $table->save($entity));
  5510. $this->assertSame(self::$nextUserId, $entity->id);
  5511. }
  5512. /**
  5513. * Tests entity clean()
  5514. */
  5515. public function testEntityClean(): void
  5516. {
  5517. $table = $this->getTableLocator()->get('Articles');
  5518. $table->getValidator()->requirePresence('body');
  5519. $entity = $table->newEntity(['title' => 'mark']);
  5520. $entity->setDirty('title', true);
  5521. $entity->setInvalidField('title', 'albert');
  5522. $this->assertNotEmpty($entity->getErrors());
  5523. $this->assertTrue($entity->isDirty());
  5524. $this->assertEquals(['title' => 'albert'], $entity->getInvalid());
  5525. $entity->title = 'alex';
  5526. $this->assertSame($entity->getOriginal('title'), 'mark');
  5527. $entity->clean();
  5528. $this->assertEmpty($entity->getErrors());
  5529. $this->assertFalse($entity->isDirty());
  5530. $this->assertEquals([], $entity->getInvalid());
  5531. $this->assertSame($entity->getOriginal('title'), 'alex');
  5532. }
  5533. /**
  5534. * Tests the loadInto() method
  5535. */
  5536. public function testLoadIntoEntity(): void
  5537. {
  5538. $table = $this->getTableLocator()->get('Authors');
  5539. $table->hasMany('SiteArticles');
  5540. $entity = $table->get(1);
  5541. $result = $table->loadInto($entity, ['SiteArticles', 'Articles.Tags']);
  5542. $this->assertSame($entity, $result);
  5543. $expected = $table->get(1, contain: ['SiteArticles', 'Articles.Tags']);
  5544. $this->assertEquals($expected->site_articles, $result->site_articles);
  5545. $this->assertEquals($expected->articles, $result->articles);
  5546. }
  5547. /**
  5548. * Tests that it is possible to pass conditions and fields to loadInto()
  5549. */
  5550. public function testLoadIntoWithConditions(): void
  5551. {
  5552. $table = $this->getTableLocator()->get('Authors');
  5553. $table->hasMany('SiteArticles');
  5554. $entity = $table->get(1);
  5555. $options = [
  5556. 'SiteArticles' => ['fields' => ['title', 'author_id']],
  5557. 'Articles.Tags' => function ($q) {
  5558. return $q->where(['Tags.name' => 'tag2']);
  5559. },
  5560. ];
  5561. $result = $table->loadInto($entity, $options);
  5562. $this->assertSame($entity, $result);
  5563. $expected = $table->get(1, contain: $options);
  5564. $this->assertEquals($expected->site_articles, $result->site_articles);
  5565. $this->assertEquals(['title', 'author_id'], $expected->site_articles[0]->getOriginalFields());
  5566. $this->assertEquals($expected->articles, $result->articles);
  5567. $this->assertSame('tag2', $expected->articles[0]->tags[0]->name);
  5568. }
  5569. /**
  5570. * Tests loadInto() with a belongsTo association
  5571. */
  5572. public function testLoadBelongsTo(): void
  5573. {
  5574. $table = $this->getTableLocator()->get('Articles');
  5575. $entity = $table->get(2);
  5576. $result = $table->loadInto($entity, ['Authors']);
  5577. $this->assertSame($entity, $result);
  5578. $expected = $table->get(2, contain: ['Authors']);
  5579. $this->assertEquals($expected, $entity);
  5580. }
  5581. /**
  5582. * Tests that it is possible to post-load associations for many entities at
  5583. * the same time
  5584. */
  5585. public function testLoadIntoMany(): void
  5586. {
  5587. $table = $this->getTableLocator()->get('Authors');
  5588. $table->hasMany('SiteArticles');
  5589. $entities = $table->find()->toArray();
  5590. $contain = ['SiteArticles', 'Articles.Tags'];
  5591. $result = $table->loadInto($entities, $contain);
  5592. foreach ($entities as $k => $v) {
  5593. $this->assertSame($v, $result[$k]);
  5594. }
  5595. $entities = $table->find()->contain($contain)->toArray();
  5596. foreach ($entities as $k => $v) {
  5597. $this->assertEquals($v->site_articles, $result[$k]->site_articles);
  5598. $this->assertEquals($v->articles, $result[$k]->articles);
  5599. }
  5600. }
  5601. /**
  5602. * Tests that saveOrFail triggers an exception on not successful save
  5603. */
  5604. public function testSaveOrFail(): void
  5605. {
  5606. $this->expectException(PersistenceFailedException::class);
  5607. $this->expectExceptionMessage('Entity save failure.');
  5608. $entity = new Entity([
  5609. 'foo' => 'bar',
  5610. ]);
  5611. $table = $this->getTableLocator()->get('users');
  5612. $table->saveOrFail($entity);
  5613. }
  5614. /**
  5615. * Tests that saveOrFail displays useful messages on output, especially in tests for CLI.
  5616. */
  5617. public function testSaveOrFailErrorDisplay(): void
  5618. {
  5619. $this->expectException(PersistenceFailedException::class);
  5620. $this->expectExceptionMessage('Entity save failure. Found the following errors (field.0: "Some message", multiple.one: "One", multiple.two: "Two")');
  5621. $entity = new Entity([
  5622. 'foo' => 'bar',
  5623. ]);
  5624. $entity->setError('field', 'Some message');
  5625. $entity->setError('multiple', ['one' => 'One', 'two' => 'Two']);
  5626. $table = $this->getTableLocator()->get('users');
  5627. $table->saveOrFail($entity);
  5628. }
  5629. /**
  5630. * Tests that saveOrFail with nested errors
  5631. */
  5632. public function testSaveOrFailNestedError(): void
  5633. {
  5634. $this->expectException(PersistenceFailedException::class);
  5635. $this->expectExceptionMessage('Entity save failure. Found the following errors (articles.0.title.0: "Bad value")');
  5636. $entity = new Entity([
  5637. 'username' => 'bad',
  5638. 'articles' => [
  5639. new Entity(['title' => 'not an entity']),
  5640. ],
  5641. ]);
  5642. $entity->articles[0]->setError('title', 'Bad value');
  5643. $table = $this->getTableLocator()->get('Users');
  5644. $table->hasMany('Articles');
  5645. $table->saveOrFail($entity);
  5646. }
  5647. /**
  5648. * Tests that saveOrFail returns the right entity
  5649. */
  5650. public function testSaveOrFailGetEntity(): void
  5651. {
  5652. $entity = new Entity([
  5653. 'foo' => 'bar',
  5654. ]);
  5655. $table = $this->getTableLocator()->get('users');
  5656. try {
  5657. $table->saveOrFail($entity);
  5658. } catch (PersistenceFailedException $e) {
  5659. $this->assertSame($entity, $e->getEntity());
  5660. }
  5661. }
  5662. /**
  5663. * Tests that deleteOrFail triggers an exception on not successful delete
  5664. */
  5665. public function testDeleteOrFail(): void
  5666. {
  5667. $this->expectException(PersistenceFailedException::class);
  5668. $this->expectExceptionMessage('Entity delete failure.');
  5669. $entity = new Entity([
  5670. 'id' => 999,
  5671. ]);
  5672. $table = $this->getTableLocator()->get('users');
  5673. $table->deleteOrFail($entity);
  5674. }
  5675. /**
  5676. * Tests that deleteOrFail returns the right entity
  5677. */
  5678. public function testDeleteOrFailGetEntity(): void
  5679. {
  5680. $entity = new Entity([
  5681. 'id' => 999,
  5682. ]);
  5683. $table = $this->getTableLocator()->get('users');
  5684. try {
  5685. $table->deleteOrFail($entity);
  5686. } catch (PersistenceFailedException $e) {
  5687. $this->assertSame($entity, $e->getEntity());
  5688. }
  5689. }
  5690. /**
  5691. * Helper method to skip tests when connection is SQLServer.
  5692. */
  5693. public function skipIfSqlServer(): void
  5694. {
  5695. $this->skipIf(
  5696. $this->connection->getDriver() instanceof Sqlserver,
  5697. 'SQLServer does not support the requirements of this test.'
  5698. );
  5699. }
  5700. }